Compare commits
5 Commits
b382688a8f
...
37cd753a6a
| Author | SHA1 | Date | |
|---|---|---|---|
| 37cd753a6a | |||
| b6a5329a6b | |||
| 230c6a958d | |||
| 0803a3bcc9 | |||
| 2f8f19d16d |
@ -30,7 +30,11 @@
|
|||||||
"Bash(ssh:*)",
|
"Bash(ssh:*)",
|
||||||
"Bash(cat:*)",
|
"Bash(cat:*)",
|
||||||
"Bash(dig:*)",
|
"Bash(dig:*)",
|
||||||
"Bash(git commit:*)"
|
"Bash(git commit:*)",
|
||||||
|
"Bash(netstat:*)",
|
||||||
|
"Bash(ss:*)",
|
||||||
|
"Bash(lsof:*)",
|
||||||
|
"Bash(dotnet run:*)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"enableAllProjectMcpServers": false
|
"enableAllProjectMcpServers": false
|
||||||
|
|||||||
36
AGENTS.md
Normal file
36
AGENTS.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Repository Guidelines
|
||||||
|
|
||||||
|
## Project Structure & Module Organization
|
||||||
|
- `src/BCards.Web` is the main MVC app (Controllers, Services, Repositories, Razor Views, `wwwroot` assets).
|
||||||
|
- `src/BCards.IntegrationTests` spins up the site with fixtures for API-level checks.
|
||||||
|
- `tests/BCards.Tests` hosts xUnit + Moq unit coverage with overrides in `appsettings.Testing.json`.
|
||||||
|
- `tests.e2e` carries Playwright specs and config; utility scripts live under `scripts/`, with `clean-build.sh` mirroring CI cleanup.
|
||||||
|
|
||||||
|
## Build, Test & Development Commands
|
||||||
|
- `dotnet restore && dotnet build BCards.sln` primes dependencies and compiles.
|
||||||
|
- `dotnet run --project src/BCards.Web` launches the site (HTTPS on 5001 by default).
|
||||||
|
- `dotnet test` executes unit + integration suites; add `--collect:"XPlat Code Coverage"` to emit coverlet results.
|
||||||
|
- In `tests.e2e`, run `npm install` once and `npx playwright test` per change; append `--headed` when debugging flows.
|
||||||
|
- `./clean-build.sh` removes stale `bin/obj` output before CI or release builds.
|
||||||
|
|
||||||
|
## Coding Style & Naming Conventions
|
||||||
|
- Use 4-space indents, file-scoped namespaces, PascalCase for types, camelCase for locals, and `_camelCase` for DI fields.
|
||||||
|
- Keep Razor views presentation-only; push logic into Services, ViewModels, or TagHelpers.
|
||||||
|
- Store localization strings in `Resources/`, shared UI in `Views/Shared`, and bundle-ready assets in `wwwroot`.
|
||||||
|
- Run `dotnet format` before pushing; .NET 8 analyzers treat warnings as errors in the pipeline.
|
||||||
|
|
||||||
|
## Testing Guidelines
|
||||||
|
- Mirror namespaces when creating unit files (`FooServiceTests` for `FooService`) and favour FluentAssertions for expressiveness.
|
||||||
|
- Integration scenarios reside in `src/BCards.IntegrationTests/Tests`; use shared fixtures to mock MongoDB/Stripe without polluting global state.
|
||||||
|
- End-to-end cases focus on signup, checkout, and profile rendering; keep snapshots in `tests.e2e/debug_*`.
|
||||||
|
- Target ≥80% coverage across `Services/` and `Repositories/`; call out gaps explicitly in the PR body.
|
||||||
|
|
||||||
|
## Commit & Pull Request Guidelines
|
||||||
|
- Follow the observed `type: resumo` format (`feat: artigos & tutoriais`, `fix: checkout`); keep scopes short, Portuguese when public-facing.
|
||||||
|
- Squash WIP branches before review; one functional change per commit.
|
||||||
|
- Every PR needs a summary, verification list (`dotnet test`, Playwright when touched), related issue link, and UI artifacts when visuals change.
|
||||||
|
- Tag a module expert for review and flip the `Ready for QA` label only after E2E automation passes.
|
||||||
|
|
||||||
|
## Security & Configuration Notes
|
||||||
|
- Keep secrets out of version control; base new configs on `appsettings.Production.example.json` and document required keys.
|
||||||
|
- When callback URLs move, update both the environment files (`Dockerfile`, `docker-compose*.yml`) and external provider dashboards together.
|
||||||
740
BCardsDB_Dev.categories.json
Normal file
740
BCardsDB_Dev.categories.json
Normal file
@ -0,0 +1,740 @@
|
|||||||
|
[{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "685b17a70138fec28779d354"
|
||||||
|
},
|
||||||
|
"name": "Corretor de Imóveis",
|
||||||
|
"slug": "corretor",
|
||||||
|
"icon": "🏠",
|
||||||
|
"seoKeywords": [
|
||||||
|
"corretor",
|
||||||
|
"imóveis",
|
||||||
|
"casa",
|
||||||
|
"apartamento",
|
||||||
|
"venda",
|
||||||
|
"locação"
|
||||||
|
],
|
||||||
|
"description": "Profissionais especializados em compra, venda e locação de imóveis",
|
||||||
|
"isActive": true,
|
||||||
|
"createdAt": {
|
||||||
|
"$date": "2025-06-24T21:24:55.336Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "685b17c20138fec28779d355"
|
||||||
|
},
|
||||||
|
"name": "Tecnologia",
|
||||||
|
"slug": "tecnologia",
|
||||||
|
"icon": "💻",
|
||||||
|
"seoKeywords": [
|
||||||
|
"desenvolvimento",
|
||||||
|
"software",
|
||||||
|
"programação",
|
||||||
|
"tecnologia",
|
||||||
|
"TI"
|
||||||
|
],
|
||||||
|
"description": "Empresas e profissionais de tecnologia, desenvolvimento e TI",
|
||||||
|
"isActive": true,
|
||||||
|
"createdAt": {
|
||||||
|
"$date": "2025-06-24T21:25:22.709Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "685b17c20138fec28779d356"
|
||||||
|
},
|
||||||
|
"name": "Saúde",
|
||||||
|
"slug": "saude",
|
||||||
|
"icon": "🏥",
|
||||||
|
"seoKeywords": [
|
||||||
|
"médico",
|
||||||
|
"saúde",
|
||||||
|
"clínica",
|
||||||
|
"consulta",
|
||||||
|
"tratamento"
|
||||||
|
],
|
||||||
|
"description": "Profissionais da saúde, clínicas e consultórios médicos",
|
||||||
|
"isActive": true,
|
||||||
|
"createdAt": {
|
||||||
|
"$date": "2025-06-24T21:25:22.713Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "685b17c20138fec28779d357"
|
||||||
|
},
|
||||||
|
"name": "Educação",
|
||||||
|
"slug": "educacao",
|
||||||
|
"icon": "📚",
|
||||||
|
"seoKeywords": [
|
||||||
|
"educação",
|
||||||
|
"ensino",
|
||||||
|
"professor",
|
||||||
|
"curso",
|
||||||
|
"escola"
|
||||||
|
],
|
||||||
|
"description": "Professores, escolas, cursos e instituições de ensino",
|
||||||
|
"isActive": true,
|
||||||
|
"createdAt": {
|
||||||
|
"$date": "2025-06-24T21:25:22.717Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "685b17c20138fec28779d358"
|
||||||
|
},
|
||||||
|
"name": "Comércio",
|
||||||
|
"slug": "comercio",
|
||||||
|
"icon": "🛍️",
|
||||||
|
"seoKeywords": [
|
||||||
|
"loja",
|
||||||
|
"comércio",
|
||||||
|
"venda",
|
||||||
|
"produtos",
|
||||||
|
"e-commerce"
|
||||||
|
],
|
||||||
|
"description": "Lojas, e-commerce e estabelecimentos comerciais",
|
||||||
|
"isActive": true,
|
||||||
|
"createdAt": {
|
||||||
|
"$date": "2025-06-24T21:25:22.720Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "685b17c20138fec28779d359"
|
||||||
|
},
|
||||||
|
"name": "Serviços",
|
||||||
|
"slug": "servicos",
|
||||||
|
"icon": "🔧",
|
||||||
|
"seoKeywords": [
|
||||||
|
"serviços",
|
||||||
|
"prestador",
|
||||||
|
"profissional",
|
||||||
|
"especializado"
|
||||||
|
],
|
||||||
|
"description": "Prestadores de serviços gerais e especializados",
|
||||||
|
"isActive": true,
|
||||||
|
"createdAt": {
|
||||||
|
"$date": "2025-06-24T21:25:22.723Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "685b17c20138fec28779d35a"
|
||||||
|
},
|
||||||
|
"name": "Alimentação",
|
||||||
|
"slug": "alimentacao",
|
||||||
|
"icon": "🍽️",
|
||||||
|
"seoKeywords": [
|
||||||
|
"restaurante",
|
||||||
|
"comida",
|
||||||
|
"delivery",
|
||||||
|
"alimentação",
|
||||||
|
"gastronomia"
|
||||||
|
],
|
||||||
|
"description": "Restaurantes, delivery, food trucks e estabelecimentos alimentícios",
|
||||||
|
"isActive": true,
|
||||||
|
"createdAt": {
|
||||||
|
"$date": "2025-06-24T21:25:22.727Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "685b17c20138fec28779d35b"
|
||||||
|
},
|
||||||
|
"name": "Beleza",
|
||||||
|
"slug": "beleza",
|
||||||
|
"icon": "💄",
|
||||||
|
"seoKeywords": [
|
||||||
|
"beleza",
|
||||||
|
"salão",
|
||||||
|
"estética",
|
||||||
|
"cabeleireiro",
|
||||||
|
"manicure"
|
||||||
|
],
|
||||||
|
"description": "Salões de beleza, barbearias, estética e cuidados pessoais",
|
||||||
|
"isActive": true,
|
||||||
|
"createdAt": {
|
||||||
|
"$date": "2025-06-24T21:25:22.731Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "685b17c20138fec28779d35c"
|
||||||
|
},
|
||||||
|
"name": "Advocacia",
|
||||||
|
"slug": "advocacia",
|
||||||
|
"icon": "⚖️",
|
||||||
|
"seoKeywords": [
|
||||||
|
"advogado",
|
||||||
|
"jurídico",
|
||||||
|
"direito",
|
||||||
|
"advocacia",
|
||||||
|
"legal"
|
||||||
|
],
|
||||||
|
"description": "Advogados, escritórios jurídicos e consultoria legal",
|
||||||
|
"isActive": true,
|
||||||
|
"createdAt": {
|
||||||
|
"$date": "2025-06-24T21:25:22.734Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "685b17c20138fec28779d35d"
|
||||||
|
},
|
||||||
|
"name": "Arquitetura",
|
||||||
|
"slug": "arquitetura",
|
||||||
|
"icon": "🏗️",
|
||||||
|
"seoKeywords": [
|
||||||
|
"arquiteto",
|
||||||
|
"engenheiro",
|
||||||
|
"construção",
|
||||||
|
"projeto",
|
||||||
|
"reforma"
|
||||||
|
],
|
||||||
|
"description": "Arquitetos, engenheiros e profissionais da construção",
|
||||||
|
"isActive": true,
|
||||||
|
"createdAt": {
|
||||||
|
"$date": "2025-06-24T21:25:22.737Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb6933"
|
||||||
|
},
|
||||||
|
"name": "Artesanato",
|
||||||
|
"slug": "artesanato",
|
||||||
|
"icon": "🎨",
|
||||||
|
"seoKeywords": [
|
||||||
|
"artesanato",
|
||||||
|
"artesão",
|
||||||
|
"feito à mão",
|
||||||
|
"personalizado",
|
||||||
|
"criativo",
|
||||||
|
"decoração"
|
||||||
|
],
|
||||||
|
"description": "Artesãos e criadores de produtos feitos à mão, decoração e arte personalizada",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb6934"
|
||||||
|
},
|
||||||
|
"name": "Papelaria",
|
||||||
|
"slug": "papelaria",
|
||||||
|
"icon": "📝",
|
||||||
|
"seoKeywords": [
|
||||||
|
"papelaria",
|
||||||
|
"escritório",
|
||||||
|
"material escolar",
|
||||||
|
"impressão",
|
||||||
|
"convites",
|
||||||
|
"personalização"
|
||||||
|
],
|
||||||
|
"description": "Lojas de papelaria, material de escritório, impressão e produtos personalizados",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb6935"
|
||||||
|
},
|
||||||
|
"name": "Coaching",
|
||||||
|
"slug": "coaching",
|
||||||
|
"icon": "🎯",
|
||||||
|
"seoKeywords": [
|
||||||
|
"coaching",
|
||||||
|
"mentoria",
|
||||||
|
"desenvolvimento pessoal",
|
||||||
|
"life coach",
|
||||||
|
"business coach",
|
||||||
|
"liderança"
|
||||||
|
],
|
||||||
|
"description": "Coaches, mentores e profissionais de desenvolvimento pessoal e empresarial",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb6936"
|
||||||
|
},
|
||||||
|
"name": "Fitness",
|
||||||
|
"slug": "fitness",
|
||||||
|
"icon": "💪",
|
||||||
|
"seoKeywords": [
|
||||||
|
"fitness",
|
||||||
|
"academia",
|
||||||
|
"personal trainer",
|
||||||
|
"musculação",
|
||||||
|
"treinamento",
|
||||||
|
"exercícios"
|
||||||
|
],
|
||||||
|
"description": "Personal trainers, academias, estúdios de pilates e profissionais fitness",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb6937"
|
||||||
|
},
|
||||||
|
"name": "Psicologia",
|
||||||
|
"slug": "psicologia",
|
||||||
|
"icon": "🧠",
|
||||||
|
"seoKeywords": [
|
||||||
|
"psicólogo",
|
||||||
|
"terapia",
|
||||||
|
"psicologia",
|
||||||
|
"saúde mental",
|
||||||
|
"consultório",
|
||||||
|
"atendimento"
|
||||||
|
],
|
||||||
|
"description": "Psicólogos, terapeutas e profissionais de saúde mental",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb6938"
|
||||||
|
},
|
||||||
|
"name": "Nutrição",
|
||||||
|
"slug": "nutricao",
|
||||||
|
"icon": "🥗",
|
||||||
|
"seoKeywords": [
|
||||||
|
"nutricionista",
|
||||||
|
"dieta",
|
||||||
|
"nutrição",
|
||||||
|
"alimentação saudável",
|
||||||
|
"consultoria nutricional",
|
||||||
|
"emagrecimento"
|
||||||
|
],
|
||||||
|
"description": "Nutricionistas, consultores em alimentação e profissionais da nutrição",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb6939"
|
||||||
|
},
|
||||||
|
"name": "Moda e Vestuário",
|
||||||
|
"slug": "moda",
|
||||||
|
"icon": "👗",
|
||||||
|
"seoKeywords": [
|
||||||
|
"moda",
|
||||||
|
"vestuário",
|
||||||
|
"roupas",
|
||||||
|
"fashion",
|
||||||
|
"estilista",
|
||||||
|
"costureira"
|
||||||
|
],
|
||||||
|
"description": "Lojas de roupas, estilistas, costureiras e profissionais da moda",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb693a"
|
||||||
|
},
|
||||||
|
"name": "Fotografia",
|
||||||
|
"slug": "fotografia",
|
||||||
|
"icon": "📸",
|
||||||
|
"seoKeywords": [
|
||||||
|
"fotógrafo",
|
||||||
|
"fotografia",
|
||||||
|
"ensaio",
|
||||||
|
"casamento",
|
||||||
|
"eventos",
|
||||||
|
"retratos"
|
||||||
|
],
|
||||||
|
"description": "Fotógrafos profissionais, estúdios fotográficos e serviços de fotografia",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb693b"
|
||||||
|
},
|
||||||
|
"name": "Marketing Digital",
|
||||||
|
"slug": "marketing-digital",
|
||||||
|
"icon": "📱",
|
||||||
|
"seoKeywords": [
|
||||||
|
"marketing digital",
|
||||||
|
"social media",
|
||||||
|
"publicidade",
|
||||||
|
"SEO",
|
||||||
|
"gestão de redes",
|
||||||
|
"digital"
|
||||||
|
],
|
||||||
|
"description": "Agências de marketing digital, gestores de redes sociais e consultores digitais",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb693c"
|
||||||
|
},
|
||||||
|
"name": "Contabilidade",
|
||||||
|
"slug": "contabilidade",
|
||||||
|
"icon": "📊",
|
||||||
|
"seoKeywords": [
|
||||||
|
"contador",
|
||||||
|
"contabilidade",
|
||||||
|
"fiscal",
|
||||||
|
"imposto de renda",
|
||||||
|
"MEI",
|
||||||
|
"consultoria contábil"
|
||||||
|
],
|
||||||
|
"description": "Contadores, escritórios contábeis e consultoria fiscal",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb693d"
|
||||||
|
},
|
||||||
|
"name": "Design",
|
||||||
|
"slug": "design",
|
||||||
|
"icon": "🎨",
|
||||||
|
"seoKeywords": [
|
||||||
|
"designer",
|
||||||
|
"design gráfico",
|
||||||
|
"identidade visual",
|
||||||
|
"logo",
|
||||||
|
"criativo",
|
||||||
|
"branding"
|
||||||
|
],
|
||||||
|
"description": "Designers gráficos, criativos e profissionais de identidade visual",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb693e"
|
||||||
|
},
|
||||||
|
"name": "Consultoria",
|
||||||
|
"slug": "consultoria",
|
||||||
|
"icon": "🤝",
|
||||||
|
"seoKeywords": [
|
||||||
|
"consultor",
|
||||||
|
"consultoria",
|
||||||
|
"assessoria",
|
||||||
|
"especialista",
|
||||||
|
"negócios",
|
||||||
|
"estratégia"
|
||||||
|
],
|
||||||
|
"description": "Consultores especializados, assessoria empresarial e serviços de consultoria",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb693f"
|
||||||
|
},
|
||||||
|
"name": "Pets",
|
||||||
|
"slug": "pets",
|
||||||
|
"icon": "🐕",
|
||||||
|
"seoKeywords": [
|
||||||
|
"veterinário",
|
||||||
|
"pet shop",
|
||||||
|
"animais",
|
||||||
|
"cuidados",
|
||||||
|
"petshop",
|
||||||
|
"adestramento"
|
||||||
|
],
|
||||||
|
"description": "Veterinários, pet shops, adestradores e serviços para animais",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb6940"
|
||||||
|
},
|
||||||
|
"name": "Casa e Jardim",
|
||||||
|
"slug": "casa-jardim",
|
||||||
|
"icon": "🏡",
|
||||||
|
"seoKeywords": [
|
||||||
|
"paisagismo",
|
||||||
|
"jardinagem",
|
||||||
|
"decoração",
|
||||||
|
"casa",
|
||||||
|
"jardim",
|
||||||
|
"plantas"
|
||||||
|
],
|
||||||
|
"description": "Paisagistas, jardineiros, decoradores e serviços para casa e jardim",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb6941"
|
||||||
|
},
|
||||||
|
"name": "Automóveis",
|
||||||
|
"slug": "automoveis",
|
||||||
|
"icon": "🚗",
|
||||||
|
"seoKeywords": [
|
||||||
|
"mecânico",
|
||||||
|
"automóveis",
|
||||||
|
"carros",
|
||||||
|
"oficina",
|
||||||
|
"manutenção",
|
||||||
|
"peças"
|
||||||
|
],
|
||||||
|
"description": "Mecânicos, oficinas, lojas de peças e serviços automotivos",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb6942"
|
||||||
|
},
|
||||||
|
"name": "Turismo",
|
||||||
|
"slug": "turismo",
|
||||||
|
"icon": "✈️",
|
||||||
|
"seoKeywords": [
|
||||||
|
"turismo",
|
||||||
|
"viagem",
|
||||||
|
"agência",
|
||||||
|
"guia turístico",
|
||||||
|
"passeios",
|
||||||
|
"hospedagem"
|
||||||
|
],
|
||||||
|
"description": "Agências de turismo, guias, pousadas e prestadores de serviços turísticos",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb6943"
|
||||||
|
},
|
||||||
|
"name": "Música",
|
||||||
|
"slug": "musica",
|
||||||
|
"icon": "🎵",
|
||||||
|
"seoKeywords": [
|
||||||
|
"músico",
|
||||||
|
"professor de música",
|
||||||
|
"instrumentos",
|
||||||
|
"aulas",
|
||||||
|
"banda",
|
||||||
|
"eventos musicais"
|
||||||
|
],
|
||||||
|
"description": "Músicos, professores de música, bandas e profissionais do entretenimento",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb6944"
|
||||||
|
},
|
||||||
|
"name": "Idiomas",
|
||||||
|
"slug": "idiomas",
|
||||||
|
"icon": "🗣️",
|
||||||
|
"seoKeywords": [
|
||||||
|
"professor de idiomas",
|
||||||
|
"inglês",
|
||||||
|
"espanhol",
|
||||||
|
"tradutor",
|
||||||
|
"aulas particulares",
|
||||||
|
"curso de idiomas"
|
||||||
|
],
|
||||||
|
"description": "Professores de idiomas, tradutores e escolas de línguas",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb6945"
|
||||||
|
},
|
||||||
|
"name": "Limpeza",
|
||||||
|
"slug": "limpeza",
|
||||||
|
"icon": "🧽",
|
||||||
|
"seoKeywords": [
|
||||||
|
"limpeza",
|
||||||
|
"faxina",
|
||||||
|
"diarista",
|
||||||
|
"higienização",
|
||||||
|
"empresa de limpeza",
|
||||||
|
"doméstica"
|
||||||
|
],
|
||||||
|
"description": "Empresas de limpeza, diaristas e serviços de higienização",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb6946"
|
||||||
|
},
|
||||||
|
"name": "Segurança",
|
||||||
|
"slug": "seguranca",
|
||||||
|
"icon": "🛡️",
|
||||||
|
"seoKeywords": [
|
||||||
|
"segurança",
|
||||||
|
"vigilante",
|
||||||
|
"porteiro",
|
||||||
|
"alarmes",
|
||||||
|
"monitoramento",
|
||||||
|
"proteção"
|
||||||
|
],
|
||||||
|
"description": "Empresas de segurança, vigilantes e serviços de proteção",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb6947"
|
||||||
|
},
|
||||||
|
"name": "Eventos",
|
||||||
|
"slug": "eventos",
|
||||||
|
"icon": "🎉",
|
||||||
|
"seoKeywords": [
|
||||||
|
"eventos",
|
||||||
|
"festa",
|
||||||
|
"casamento",
|
||||||
|
"buffet",
|
||||||
|
"decoração de festas",
|
||||||
|
"cerimonial"
|
||||||
|
],
|
||||||
|
"description": "Organizadores de eventos, buffets, decoração e cerimonial",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb6948"
|
||||||
|
},
|
||||||
|
"name": "Transporte",
|
||||||
|
"slug": "transporte",
|
||||||
|
"icon": "🚐",
|
||||||
|
"seoKeywords": [
|
||||||
|
"transporte",
|
||||||
|
"frete",
|
||||||
|
"mudança",
|
||||||
|
"delivery",
|
||||||
|
"motorista",
|
||||||
|
"logística"
|
||||||
|
],
|
||||||
|
"description": "Empresas de transporte, fretes, mudanças e serviços de entrega",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb6949"
|
||||||
|
},
|
||||||
|
"name": "Construção",
|
||||||
|
"slug": "construcao",
|
||||||
|
"icon": "🔨",
|
||||||
|
"seoKeywords": [
|
||||||
|
"construção",
|
||||||
|
"pedreiro",
|
||||||
|
"pintor",
|
||||||
|
"eletricista",
|
||||||
|
"encanador",
|
||||||
|
"reforma"
|
||||||
|
],
|
||||||
|
"description": "Profissionais da construção civil, reformas e manutenção predial",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb694a"
|
||||||
|
},
|
||||||
|
"name": "Joias e Acessórios",
|
||||||
|
"slug": "joias",
|
||||||
|
"icon": "💎",
|
||||||
|
"seoKeywords": [
|
||||||
|
"joias",
|
||||||
|
"bijuterias",
|
||||||
|
"acessórios",
|
||||||
|
"ourives",
|
||||||
|
"relógios",
|
||||||
|
"semijoias"
|
||||||
|
],
|
||||||
|
"description": "Joalherias, bijuterias, ourives e lojas de acessórios",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb694b"
|
||||||
|
},
|
||||||
|
"name": "Odontologia",
|
||||||
|
"slug": "odontologia",
|
||||||
|
"icon": "🦷",
|
||||||
|
"seoKeywords": [
|
||||||
|
"dentista",
|
||||||
|
"odontologia",
|
||||||
|
"clínica dentária",
|
||||||
|
"ortodontia",
|
||||||
|
"implante",
|
||||||
|
"oral"
|
||||||
|
],
|
||||||
|
"description": "Dentistas, clínicas odontológicas e profissionais da área bucal",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb694c"
|
||||||
|
},
|
||||||
|
"name": "Fisioterapia",
|
||||||
|
"slug": "fisioterapia",
|
||||||
|
"icon": "🏥",
|
||||||
|
"seoKeywords": [
|
||||||
|
"fisioterapeuta",
|
||||||
|
"fisioterapia",
|
||||||
|
"reabilitação",
|
||||||
|
"RPG",
|
||||||
|
"massagem",
|
||||||
|
"terapia"
|
||||||
|
],
|
||||||
|
"description": "Fisioterapeutas, clínicas de reabilitação e terapias corporais",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb694d"
|
||||||
|
},
|
||||||
|
"name": "Livraria",
|
||||||
|
"slug": "livraria",
|
||||||
|
"icon": "📚",
|
||||||
|
"seoKeywords": [
|
||||||
|
"livraria",
|
||||||
|
"livros",
|
||||||
|
"sebo",
|
||||||
|
"literatura",
|
||||||
|
"editora",
|
||||||
|
"publicação"
|
||||||
|
],
|
||||||
|
"description": "Livrarias, sebos, editoras e comércio de livros e publicações",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb694e"
|
||||||
|
},
|
||||||
|
"name": "Floricultura",
|
||||||
|
"slug": "floricultura",
|
||||||
|
"icon": "🌸",
|
||||||
|
"seoKeywords": [
|
||||||
|
"floricultura",
|
||||||
|
"flores",
|
||||||
|
"buquê",
|
||||||
|
"plantas",
|
||||||
|
"arranjos",
|
||||||
|
"casamento"
|
||||||
|
],
|
||||||
|
"description": "Floriculturas, arranjos florais e comércio de plantas ornamentais",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb694f"
|
||||||
|
},
|
||||||
|
"name": "Farmácia",
|
||||||
|
"slug": "farmacia",
|
||||||
|
"icon": "💊",
|
||||||
|
"seoKeywords": [
|
||||||
|
"farmácia",
|
||||||
|
"farmacêutico",
|
||||||
|
"medicamentos",
|
||||||
|
"drogaria",
|
||||||
|
"manipulação",
|
||||||
|
"remédios"
|
||||||
|
],
|
||||||
|
"description": "Farmácias, drogarias e farmacêuticos especializados",
|
||||||
|
"isActive": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": {
|
||||||
|
"$oid": "68c1bd62b1e9f4be8ecb6950"
|
||||||
|
},
|
||||||
|
"name": "Delivery",
|
||||||
|
"slug": "delivery",
|
||||||
|
"icon": "🛵",
|
||||||
|
"seoKeywords": [
|
||||||
|
"delivery",
|
||||||
|
"entrega",
|
||||||
|
"motoboy",
|
||||||
|
"comida",
|
||||||
|
"aplicativo",
|
||||||
|
"rápido"
|
||||||
|
],
|
||||||
|
"description": "Serviços de delivery, entregadores e aplicativos de entrega",
|
||||||
|
"isActive": true
|
||||||
|
}]
|
||||||
394
Content/Artigos/bcards-vs-linktree.pt-BR.md
Normal file
394
Content/Artigos/bcards-vs-linktree.pt-BR.md
Normal file
@ -0,0 +1,394 @@
|
|||||||
|
---
|
||||||
|
title: "BCards vs LinkTree: A Alternativa Brasileira para Profissionais"
|
||||||
|
description: "Descubra por que o BCards é a melhor alternativa brasileira ao LinkTree para profissionais e empresas. Comparação completa de recursos, preços e diferenciais."
|
||||||
|
keywords: "linktree, link tree, alternativa ao linktree, melhor que linktree, bcards, cartão digital, bio links, página de links, linktree brasil, linktree brasileiro"
|
||||||
|
author: "BCards"
|
||||||
|
date: 2025-11-02
|
||||||
|
lastmod: 2025-11-02
|
||||||
|
image: "/images/artigos/bcards-vs-linktree-hero.jpg"
|
||||||
|
---
|
||||||
|
|
||||||
|
# BCards vs LinkTree: A Alternativa Brasileira para Profissionais
|
||||||
|
|
||||||
|
Se você está procurando uma alternativa ao LinkTree, você chegou ao lugar certo. O BCards é uma solução brasileira desenvolvida especificamente para profissionais e empresas que querem uma presença digital profissional sem depender de plataformas internacionais.
|
||||||
|
|
||||||
|
## O Que São Páginas de Bio Links?
|
||||||
|
|
||||||
|
Antes de compararmos, vamos entender o conceito. Ferramentas como LinkTree e BCards são páginas de "bio links" - uma única URL que centraliza todos os seus links importantes:
|
||||||
|
|
||||||
|
- Redes sociais (Instagram, LinkedIn, Facebook)
|
||||||
|
- WhatsApp para contato direto
|
||||||
|
- Site ou portfólio
|
||||||
|
- Loja online ou produtos
|
||||||
|
- Localização física
|
||||||
|
- E muito mais
|
||||||
|
|
||||||
|
A ideia é simples: ao invés de ter apenas um link na bio do Instagram, você tem uma página com todos os seus links organizados.
|
||||||
|
|
||||||
|
## Por Que Escolher uma Alternativa Brasileira ao LinkTree?
|
||||||
|
|
||||||
|
### 🇧🇷 1. Suporte em Português (de Verdade)
|
||||||
|
|
||||||
|
**LinkTree:**
|
||||||
|
- Suporte majoritariamente em inglês
|
||||||
|
- Fuso horário dos EUA
|
||||||
|
- Documentação traduzida automaticamente
|
||||||
|
|
||||||
|
**BCards:**
|
||||||
|
- Suporte 100% em português
|
||||||
|
- Atendimento no horário comercial brasileiro
|
||||||
|
- Documentação escrita para o mercado brasileiro
|
||||||
|
- Entendimento de necessidades locais
|
||||||
|
|
||||||
|
### 💰 2. Preços em Real (Sem Surpresas)
|
||||||
|
|
||||||
|
**LinkTree:**
|
||||||
|
- Preços em dólar (USD)
|
||||||
|
- Sujeito a variação cambial
|
||||||
|
- IOF em pagamentos internacionais (6.38%)
|
||||||
|
- Preço final imprevisível
|
||||||
|
|
||||||
|
**BCards:**
|
||||||
|
- Preços fixos em Real (R$)
|
||||||
|
- Sem variação cambial
|
||||||
|
- Sem taxas de IOF
|
||||||
|
- Pagamento via Pix, cartão brasileiro
|
||||||
|
|
||||||
|
**Exemplo Real:**
|
||||||
|
- LinkTree Pro: $9 USD/mês ≈ R$ 45-50 (variável)
|
||||||
|
- BCards Básico: R$ 12,90/mês (fixo)
|
||||||
|
|
||||||
|
### 🎯 3. URLs Profissionais e Semânticas
|
||||||
|
|
||||||
|
**LinkTree:**
|
||||||
|
```
|
||||||
|
linktr.ee/seunome
|
||||||
|
```
|
||||||
|
- Domínio genérico
|
||||||
|
- Não transmite profissionalismo
|
||||||
|
- Óbvio que é uma ferramenta de terceiros
|
||||||
|
|
||||||
|
**BCards:**
|
||||||
|
```
|
||||||
|
bcards.site/page/tecnologia/seunome
|
||||||
|
bcards.site/page/advocacia/drjoao
|
||||||
|
bcards.site/page/saude/drmaria
|
||||||
|
```
|
||||||
|
- URLs categorizadas por profissão
|
||||||
|
- SEO otimizado com categoria
|
||||||
|
- Aparência mais profissional
|
||||||
|
|
||||||
|
## Comparação Técnica: LinkTree vs BCards
|
||||||
|
|
||||||
|
### Recursos Disponíveis
|
||||||
|
|
||||||
|
| Recurso | LinkTree Free | LinkTree Pro ($9/mês) | BCards Grátis | BCards Básico (R$ 12,90/mês) |
|
||||||
|
|---------|---------------|----------------------|---------------|----------------------------|
|
||||||
|
| **Número de Links** | Ilimitado | Ilimitado | 5 | 15 |
|
||||||
|
| **Temas** | Básicos | Avançados | Básicos | Todos |
|
||||||
|
| **Analytics** | Básico | Detalhado | Básico | Detalhado |
|
||||||
|
| **Remover Logo** | ❌ | ✅ | ✅ (Grátis!) | ✅ |
|
||||||
|
| **URLs Customizáveis** | Limitado | ✅ | ✅ | ✅ |
|
||||||
|
| **Suporte em Português** | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| **Pagamento em Real** | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| **Categorização por Profissão** | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
| **Moderação Humana** | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
|
||||||
|
### Diferenciais Técnicos do BCards
|
||||||
|
|
||||||
|
#### 1. Sistema de Categorias Profissionais
|
||||||
|
|
||||||
|
O BCards organiza perfis por categoria profissional, o que oferece vantagens:
|
||||||
|
|
||||||
|
**SEO:**
|
||||||
|
- Google indexa melhor URLs categorizadas
|
||||||
|
- Buscar "advogado + cidade" pode trazer seu BCard
|
||||||
|
- Autoridade topical (especialização por nicho)
|
||||||
|
|
||||||
|
**Profissionalismo:**
|
||||||
|
- URL mostra sua área de atuação
|
||||||
|
- Facilita encontrar profissionais similares
|
||||||
|
- Networking dentro da categoria
|
||||||
|
|
||||||
|
**Exemplos:**
|
||||||
|
```
|
||||||
|
bcards.site/page/advocacia/dr-pedro-silva
|
||||||
|
bcards.site/page/tecnologia/joao-dev
|
||||||
|
bcards.site/page/saude/dra-ana-cardiologia
|
||||||
|
bcards.site/page/beleza/salao-mariana
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Moderação Humana (Sem Conteúdo Inapropriado)
|
||||||
|
|
||||||
|
**LinkTree:**
|
||||||
|
- Moderação automática (quando há)
|
||||||
|
- Permite conteúdo adulto em alguns planos
|
||||||
|
- Risco de associação com perfis inadequados
|
||||||
|
|
||||||
|
**BCards:**
|
||||||
|
- Moderação humana de todos os perfis
|
||||||
|
- Políticas claras de conteúdo profissional
|
||||||
|
- Ambiente seguro para profissionais sérios
|
||||||
|
- Rejeição de conteúdo inapropriado
|
||||||
|
|
||||||
|
**Por que isso importa?**
|
||||||
|
- Sua marca não fica ao lado de conteúdo duvidoso
|
||||||
|
- Transmite profissionalismo
|
||||||
|
- Ideal para advogados, médicos, contadores, etc.
|
||||||
|
|
||||||
|
#### 3. Integração com Mercado Brasileiro
|
||||||
|
|
||||||
|
**LinkTree:**
|
||||||
|
- Links para Venmo, CashApp (EUA)
|
||||||
|
- Integração com plataformas americanas
|
||||||
|
- Checkout Shopify (internacional)
|
||||||
|
|
||||||
|
**BCards:**
|
||||||
|
- Foco em WhatsApp Business (essencial no Brasil)
|
||||||
|
- Integração futura com Pix
|
||||||
|
- Mercado Livre, Mercado Pago, PagSeguro
|
||||||
|
- Google Maps para localização (crucial para negócios locais)
|
||||||
|
|
||||||
|
## Casos de Uso: Quando BCards é Melhor?
|
||||||
|
|
||||||
|
### ✅ Ideal para BCards:
|
||||||
|
|
||||||
|
1. **Advogados e Profissionais Liberais**
|
||||||
|
- URL profissional com categoria
|
||||||
|
- Moderação garante ambiente sério
|
||||||
|
- Preço acessível em Real
|
||||||
|
|
||||||
|
2. **Pequenos Negócios Locais**
|
||||||
|
- Integração com Google Maps
|
||||||
|
- WhatsApp Business como principal contato
|
||||||
|
- Preços em moeda local
|
||||||
|
|
||||||
|
3. **Profissionais de Saúde**
|
||||||
|
- Ambiente moderado e profissional
|
||||||
|
- Links para agendamento local
|
||||||
|
- Conformidade com políticas do CFM
|
||||||
|
|
||||||
|
4. **Prestadores de Serviços**
|
||||||
|
- Categorização por área
|
||||||
|
- Fácil de compartilhar localmente
|
||||||
|
- Suporte em português
|
||||||
|
|
||||||
|
### 🤔 Quando Considerar LinkTree:
|
||||||
|
|
||||||
|
1. **Influenciadores Internacionais**
|
||||||
|
- Audiência global
|
||||||
|
- Receita em dólar
|
||||||
|
- Integrações com plataformas americanas
|
||||||
|
|
||||||
|
2. **E-commerce Internacional**
|
||||||
|
- Vendas para fora do Brasil
|
||||||
|
- Shopify, Amazon global
|
||||||
|
- Múltiplas moedas
|
||||||
|
|
||||||
|
## Recursos Exclusivos do BCards
|
||||||
|
|
||||||
|
### 1. Links de Produtos com Preview Automático
|
||||||
|
|
||||||
|
Adicione links de produtos e o BCards captura automaticamente:
|
||||||
|
- Imagem do produto
|
||||||
|
- Preço
|
||||||
|
- Descrição
|
||||||
|
- Botão direto para compra
|
||||||
|
|
||||||
|
**Suportado:**
|
||||||
|
- Mercado Livre
|
||||||
|
- Shopee
|
||||||
|
- Amazon Brasil
|
||||||
|
- Lojas com Open Graph
|
||||||
|
|
||||||
|
### 2. Sistema de Preview para Moderação
|
||||||
|
|
||||||
|
Antes de seu perfil ficar público:
|
||||||
|
- Revisão humana do conteúdo
|
||||||
|
- Feedback se algo precisa ser ajustado
|
||||||
|
- Link de preview para você testar
|
||||||
|
- Aprovação rápida (geralmente em 24h)
|
||||||
|
|
||||||
|
### 3. Analytics Focado no Mercado Brasileiro
|
||||||
|
|
||||||
|
**Métricas relevantes:**
|
||||||
|
- Cliques por link
|
||||||
|
- Horários de maior acesso (fuso brasileiro)
|
||||||
|
- Origem do tráfego (Instagram, WhatsApp, etc.)
|
||||||
|
- Dispositivos (maioria mobile no Brasil)
|
||||||
|
|
||||||
|
## Preços Comparados (Detalhado)
|
||||||
|
|
||||||
|
### LinkTree
|
||||||
|
|
||||||
|
**Free:**
|
||||||
|
- Links ilimitados
|
||||||
|
- Temas básicos
|
||||||
|
- Analytics limitado
|
||||||
|
- Logo LinkTree presente
|
||||||
|
|
||||||
|
**Pro ($9 USD/mês ≈ R$ 45-50):**
|
||||||
|
- Temas premium
|
||||||
|
- Analytics avançado
|
||||||
|
- Remove logo
|
||||||
|
- Links prioritários
|
||||||
|
- **Problema:** Preço varia com dólar + IOF
|
||||||
|
|
||||||
|
**Premium ($24 USD/mês ≈ R$ 120-135):**
|
||||||
|
- Tudo do Pro
|
||||||
|
- Integrações avançadas
|
||||||
|
- Suporte prioritário
|
||||||
|
|
||||||
|
### BCards
|
||||||
|
|
||||||
|
**Gratuito:**
|
||||||
|
- 5 links
|
||||||
|
- Temas básicos
|
||||||
|
- Analytics básico
|
||||||
|
- **Sem logo BCards** (diferencial!)
|
||||||
|
|
||||||
|
**Básico (R$ 12,90/mês fixo):**
|
||||||
|
- 15 links
|
||||||
|
- Todos os temas
|
||||||
|
- Analytics detalhado
|
||||||
|
- Suporte em português
|
||||||
|
|
||||||
|
**Premium (R$ 29,90/mês fixo):**
|
||||||
|
- Links ilimitados
|
||||||
|
- Temas personalizados
|
||||||
|
- Logo personalizado
|
||||||
|
- Cores customizadas
|
||||||
|
- Suporte prioritário
|
||||||
|
- Upload de PDFs (até 5 arquivos)
|
||||||
|
|
||||||
|
**Economia Real:**
|
||||||
|
- LinkTree Pro anual: ~$108 + IOF = ~R$ 540-600
|
||||||
|
- BCards Básico anual: R$ 118,80
|
||||||
|
- **Economia: ~R$ 420/ano**
|
||||||
|
|
||||||
|
## Migração do LinkTree para BCards
|
||||||
|
|
||||||
|
### É Fácil Migrar?
|
||||||
|
|
||||||
|
**Sim! Processo simples:**
|
||||||
|
|
||||||
|
1. **Crie sua conta BCards** (2 minutos)
|
||||||
|
2. **Copie seus links do LinkTree** (5 minutos)
|
||||||
|
3. **Cole no BCards** (5 minutos)
|
||||||
|
4. **Ajuste tema e cores** (5 minutos)
|
||||||
|
5. **Atualize bio das redes sociais** (2 minutos)
|
||||||
|
|
||||||
|
**Total: ~20 minutos**
|
||||||
|
|
||||||
|
### Posso Testar Antes de Migrar?
|
||||||
|
|
||||||
|
Sim! O plano gratuito do BCards permite:
|
||||||
|
- Criar perfil completo
|
||||||
|
- Testar 5 links principais
|
||||||
|
- Ver como funciona
|
||||||
|
- Decidir se vale a pena
|
||||||
|
|
||||||
|
**Sem risco:**
|
||||||
|
- Não precisa cancelar LinkTree antes
|
||||||
|
- Teste paralelamente
|
||||||
|
- Migre quando se sentir confortável
|
||||||
|
|
||||||
|
## Perguntas Frequentes: BCards vs LinkTree
|
||||||
|
|
||||||
|
### O BCards funciona com Instagram, TikTok, YouTube?
|
||||||
|
|
||||||
|
Sim! Funciona com qualquer rede social que permite um link na bio:
|
||||||
|
- Instagram ✅
|
||||||
|
- TikTok ✅
|
||||||
|
- YouTube ✅
|
||||||
|
- LinkedIn ✅
|
||||||
|
- Facebook ✅
|
||||||
|
- Twitter/X ✅
|
||||||
|
|
||||||
|
### Posso usar domínio próprio?
|
||||||
|
|
||||||
|
**LinkTree:** Apenas em planos premium ($24/mês)
|
||||||
|
**BCards:** Em desenvolvimento (plano Premium futuro)
|
||||||
|
|
||||||
|
Atualmente: `bcards.site/page/categoria/seunome`
|
||||||
|
|
||||||
|
### E se eu já tenho linktr.ee nas minhas mídias impressas?
|
||||||
|
|
||||||
|
Você pode:
|
||||||
|
1. Manter LinkTree redirecionando para BCards temporariamente
|
||||||
|
2. Atualizar gradualmente materiais impressos
|
||||||
|
3. Usar QR Code do BCards (gera automaticamente)
|
||||||
|
|
||||||
|
### O BCards tem app mobile?
|
||||||
|
|
||||||
|
Não é necessário! É 100% web:
|
||||||
|
- Funciona perfeitamente no mobile
|
||||||
|
- Não ocupa espaço no celular
|
||||||
|
- Atualiza instantaneamente
|
||||||
|
- Sem necessidade de instalar nada
|
||||||
|
|
||||||
|
### Posso cancelar a qualquer momento?
|
||||||
|
|
||||||
|
Sim! Sem fidelidade:
|
||||||
|
- Cancele quando quiser
|
||||||
|
- Sem multa
|
||||||
|
- Sem burocracia
|
||||||
|
- Dados não são deletados (voltam ao plano grátis)
|
||||||
|
|
||||||
|
## Conclusão: Vale a Pena Trocar?
|
||||||
|
|
||||||
|
### ✅ Vale a pena trocar do LinkTree para BCards se:
|
||||||
|
|
||||||
|
- Você é profissional ou empresa brasileira
|
||||||
|
- Quer economia real (3-5x mais barato)
|
||||||
|
- Prefere suporte em português
|
||||||
|
- Busca URL mais profissional
|
||||||
|
- Quer ambiente moderado e sério
|
||||||
|
- Precisa de pagamento em Real sem surpresas
|
||||||
|
|
||||||
|
### 🤔 Talvez LinkTree seja melhor se:
|
||||||
|
|
||||||
|
- Sua audiência é majoritariamente internacional
|
||||||
|
- Você vende em dólar
|
||||||
|
- Precisa integrações específicas americanas
|
||||||
|
- Já tem grande investimento na marca "linktr.ee"
|
||||||
|
|
||||||
|
## Comece Agora
|
||||||
|
|
||||||
|
### Teste Gratuitamente
|
||||||
|
|
||||||
|
Não precisa acreditar na nossa palavra. Teste você mesmo:
|
||||||
|
|
||||||
|
1. **Crie conta grátis** (sem cartão de crédito)
|
||||||
|
2. **Configure seus 5 links principais**
|
||||||
|
3. **Escolha um tema profissional**
|
||||||
|
4. **Compartilhe e veja os resultados**
|
||||||
|
|
||||||
|
Se gostar, faça upgrade para ter mais links. Se não gostar, não paga nada.
|
||||||
|
|
||||||
|
**Pronto para dar o próximo passo?** [Criar meu BCard grátis](https://bcards.site)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comparação Rápida (TL;DR)
|
||||||
|
|
||||||
|
| Aspecto | LinkTree | BCards |
|
||||||
|
|---------|----------|--------|
|
||||||
|
| **Preço (plano básico)** | ~R$ 45-50/mês (varia) | R$ 12,90/mês (fixo) |
|
||||||
|
| **Suporte** | Inglês | Português |
|
||||||
|
| **Pagamento** | Dólar + IOF | Real (Pix/Cartão BR) |
|
||||||
|
| **URL** | linktr.ee/nome | bcards.site/categoria/nome |
|
||||||
|
| **Moderação** | Automática | Humana |
|
||||||
|
| **Foco** | Global | Brasil |
|
||||||
|
| **Remover logo (grátis)** | ❌ | ✅ |
|
||||||
|
|
||||||
|
**Economia anual:** ~R$ 420 escolhendo BCards
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recursos Adicionais
|
||||||
|
|
||||||
|
- [Tutorial: Como Criar um BCard](/tutoriais/tecnologia/como-criar-um-bcard)
|
||||||
|
- [BCards para Advogados](/tutoriais/advocacia/como-advogados-podem-usar-bcards)
|
||||||
|
- [Transformação Digital](/artigos/transformacao-digital-pequenos-negocios)
|
||||||
|
- [Fale com nosso suporte](/support)
|
||||||
208
Content/Artigos/transformacao-digital-pequenos-negocios.pt-BR.md
Normal file
208
Content/Artigos/transformacao-digital-pequenos-negocios.pt-BR.md
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
---
|
||||||
|
title: "Transformação Digital para Pequenos Negócios: Por Onde Começar?"
|
||||||
|
description: "Descubra como a transformação digital pode revolucionar seu pequeno negócio e quais são os primeiros passos para entrar no mundo digital"
|
||||||
|
keywords: "transformação digital, pequenos negócios, digitalização, empreendedorismo, tecnologia"
|
||||||
|
author: "BCards"
|
||||||
|
date: 2025-11-02
|
||||||
|
lastmod: 2025-11-02
|
||||||
|
image: "/images/artigos/transformacao-digital-hero.jpg"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Transformação Digital para Pequenos Negócios: Por Onde Começar?
|
||||||
|
|
||||||
|
A transformação digital não é mais uma opção, mas uma necessidade para pequenos negócios que querem se manter competitivos. Mas o que exatamente significa "transformação digital" e como um pequeno negócio pode embarcar nessa jornada sem gastar fortunas?
|
||||||
|
|
||||||
|
## O Que É Transformação Digital?
|
||||||
|
|
||||||
|
Transformação digital é o processo de usar tecnologias digitais para criar novos processos de negócio, cultura e experiências do cliente — ou modificar os existentes — para atender às mudanças nas demandas do mercado.
|
||||||
|
|
||||||
|
Para pequenos negócios, isso pode ser tão simples quanto:
|
||||||
|
- Ter uma presença online profissional
|
||||||
|
- Aceitar pagamentos digitais
|
||||||
|
- Usar redes sociais para marketing
|
||||||
|
- Digitalizar processos internos
|
||||||
|
- Oferecer atendimento online
|
||||||
|
|
||||||
|
## Por Que Pequenos Negócios Precisam Se Digitalizar?
|
||||||
|
|
||||||
|
### 1. Alcance Maior
|
||||||
|
|
||||||
|
Com presença digital, você não está mais limitado à sua localização física. Clientes de qualquer lugar podem encontrar e conhecer seu negócio.
|
||||||
|
|
||||||
|
### 2. Custos Menores
|
||||||
|
|
||||||
|
Marketing digital é significativamente mais barato que publicidade tradicional. Uma campanha no Instagram pode custar centenas de vezes menos que um anúncio em jornal.
|
||||||
|
|
||||||
|
### 3. Competitividade
|
||||||
|
|
||||||
|
Seus concorrentes já estão online. Não estar presente digitalmente significa perder clientes para quem está.
|
||||||
|
|
||||||
|
### 4. Conveniência para o Cliente
|
||||||
|
|
||||||
|
Clientes modernos esperam poder:
|
||||||
|
- Encontrar informações online
|
||||||
|
- Fazer pedidos pelo WhatsApp
|
||||||
|
- Pagar com Pix ou cartão
|
||||||
|
- Ver avaliações de outros clientes
|
||||||
|
|
||||||
|
## Os 5 Primeiros Passos da Transformação Digital
|
||||||
|
|
||||||
|
### 1. Crie uma Presença Digital Básica
|
||||||
|
|
||||||
|
Você não precisa de um site complexo. Comece com:
|
||||||
|
|
||||||
|
**Mínimo Necessário:**
|
||||||
|
- Uma página de links profissional (como o BCards)
|
||||||
|
- Perfil no Instagram ou Facebook
|
||||||
|
- WhatsApp Business configurado
|
||||||
|
|
||||||
|
**Por que isso funciona:**
|
||||||
|
- É gratuito ou muito barato
|
||||||
|
- Pode ser feito em 1 dia
|
||||||
|
- Já coloca você no mapa digital
|
||||||
|
|
||||||
|
### 2. Organize Seus Contatos Digitais
|
||||||
|
|
||||||
|
Centralize todos os seus pontos de contato:
|
||||||
|
- WhatsApp para pedidos
|
||||||
|
- Instagram para divulgação
|
||||||
|
- Email para orçamentos
|
||||||
|
- Link para catálogo de produtos
|
||||||
|
|
||||||
|
**Solução simples:** Use um cartão digital (BCard) que concentra todos esses links em um só lugar. Assim, você compartilha um único link e o cliente escolhe como prefere contatar você.
|
||||||
|
|
||||||
|
### 3. Aceite Pagamentos Digitais
|
||||||
|
|
||||||
|
Configure:
|
||||||
|
- Pix (essencial!)
|
||||||
|
- Maquininha de cartão
|
||||||
|
- Link de pagamento (Mercado Pago, PagSeguro)
|
||||||
|
|
||||||
|
**Resultado:** Você não perde vendas porque "o cliente não tem dinheiro trocado".
|
||||||
|
|
||||||
|
### 4. Use as Redes Sociais Estrategicamente
|
||||||
|
|
||||||
|
Não precisa estar em todas. Escolha UMA ou DUAS e faça bem:
|
||||||
|
|
||||||
|
**Para produtos físicos:** Instagram + Facebook
|
||||||
|
**Para serviços profissionais:** LinkedIn + Instagram
|
||||||
|
**Para comércio local:** Facebook + Google Meu Negócio
|
||||||
|
|
||||||
|
**Dica de ouro:** Poste regularmente (mesmo que seja 2-3x por semana). Consistência > Quantidade.
|
||||||
|
|
||||||
|
### 5. Peça Avaliações e Depoimentos
|
||||||
|
|
||||||
|
Clientes satisfeitos são seu melhor marketing. Peça:
|
||||||
|
- Avaliações no Google
|
||||||
|
- Comentários no Instagram
|
||||||
|
- Depoimentos em vídeo (WhatsApp)
|
||||||
|
|
||||||
|
**Como pedir:** "Se você ficou satisfeito com nosso serviço, pode deixar uma avaliação no Google? Isso nos ajuda muito!"
|
||||||
|
|
||||||
|
## Ferramentas Essenciais (e Baratas!)
|
||||||
|
|
||||||
|
### Gratuitas
|
||||||
|
- **Google Meu Negócio** - Apareça no Google Maps
|
||||||
|
- **WhatsApp Business** - Atendimento profissional
|
||||||
|
- **Canva** - Design de posts e materiais
|
||||||
|
- **BCards** - Página de links profissional (plano grátis disponível)
|
||||||
|
|
||||||
|
### Investimento Baixo (menos de R$ 50/mês)
|
||||||
|
- **Instagram Ads** - Impulsione suas publicações
|
||||||
|
- **Hotmart/Eduzz** - Venda produtos digitais
|
||||||
|
- **Mercado Pago** - Links de pagamento
|
||||||
|
- **BCards Premium** - Cartão digital profissional completo
|
||||||
|
|
||||||
|
## Erros Comuns a Evitar
|
||||||
|
|
||||||
|
### ❌ Erro 1: Querer Fazer Tudo de Uma Vez
|
||||||
|
|
||||||
|
Não tente criar site, app, loja virtual e estar em todas as redes sociais simultaneamente. Comece pequeno e expanda gradualmente.
|
||||||
|
|
||||||
|
### ❌ Erro 2: Comprar Ferramentas Caras Demais
|
||||||
|
|
||||||
|
Você não precisa de um site de R$ 10.000 quando está começando. Use ferramentas simples primeiro e evolua conforme cresce.
|
||||||
|
|
||||||
|
### ❌ Erro 3: Não Ter um Ponto Central
|
||||||
|
|
||||||
|
Ter Instagram, Facebook, WhatsApp é ótimo. Mas onde você manda as pessoas? Tenha UM lugar que centraliza tudo (um BCard, por exemplo).
|
||||||
|
|
||||||
|
### ❌ Erro 4: Não Medir Resultados
|
||||||
|
|
||||||
|
Use as ferramentas gratuitas de analytics:
|
||||||
|
- Instagram Insights
|
||||||
|
- Google Analytics
|
||||||
|
- Relatórios do WhatsApp Business
|
||||||
|
|
||||||
|
## Casos de Sucesso Inspiradores
|
||||||
|
|
||||||
|
### Padaria da Dona Maria
|
||||||
|
|
||||||
|
**Antes:** Apenas loja física, dependia de clientes que passavam na frente
|
||||||
|
**Depois:**
|
||||||
|
- BCard com cardápio e WhatsApp
|
||||||
|
- QR Code nas embalagens
|
||||||
|
- Pedidos pelo WhatsApp aumentaram 60%
|
||||||
|
- Clientes de bairros vizinhos começaram a pedir
|
||||||
|
|
||||||
|
### Consultório do Dr. Pedro (Dentista)
|
||||||
|
|
||||||
|
**Antes:** Agendamentos apenas por telefone em horário comercial
|
||||||
|
**Depois:**
|
||||||
|
- Sistema de agendamento online
|
||||||
|
- BCard com links para WhatsApp, Google Maps e Instagram
|
||||||
|
- Reduziu trabalho da secretária em 40%
|
||||||
|
- Mais agendamentos fora do horário comercial
|
||||||
|
|
||||||
|
### Loja de Roupas da Ana
|
||||||
|
|
||||||
|
**Antes:** Vendas apenas presenciais
|
||||||
|
**Depois:**
|
||||||
|
- Catálogo no Instagram
|
||||||
|
- BCard com link para catálogo completo
|
||||||
|
- WhatsApp Business para pedidos
|
||||||
|
- Vendas online representam 35% do faturamento
|
||||||
|
|
||||||
|
## Próximos Passos Para Seu Negócio
|
||||||
|
|
||||||
|
### Semana 1: Organização
|
||||||
|
- Liste todos os seus pontos de contato atuais
|
||||||
|
- Crie um BCard com todos os links
|
||||||
|
- Configure WhatsApp Business
|
||||||
|
|
||||||
|
### Semana 2: Presença Online
|
||||||
|
- Atualize perfis de redes sociais
|
||||||
|
- Adicione o BCard na bio
|
||||||
|
- Tire fotos profissionais dos produtos/serviços
|
||||||
|
|
||||||
|
### Semana 3: Divulgação
|
||||||
|
- Crie QR Code do seu BCard
|
||||||
|
- Coloque em materiais impressos
|
||||||
|
- Peça avaliações de clientes fiéis
|
||||||
|
|
||||||
|
### Semana 4: Otimização
|
||||||
|
- Analise o que funcionou
|
||||||
|
- Ajuste e melhore
|
||||||
|
- Planeje próximos passos
|
||||||
|
|
||||||
|
## Conclusão
|
||||||
|
|
||||||
|
A transformação digital para pequenos negócios não precisa ser cara ou complicada. Comece com o básico:
|
||||||
|
|
||||||
|
1. Presença online simples (BCard + redes sociais)
|
||||||
|
2. Facilite o contato (WhatsApp Business)
|
||||||
|
3. Aceite pagamentos digitais (Pix + cartão)
|
||||||
|
4. Peça avaliações de clientes
|
||||||
|
5. Evolua gradualmente
|
||||||
|
|
||||||
|
O importante é começar. Cada pequeno passo digital que você dá coloca seu negócio à frente dos concorrentes que ainda não se moveram.
|
||||||
|
|
||||||
|
**Pronto para dar o primeiro passo?** [Crie seu BCard profissional grátis](https://bcards.site) e comece sua transformação digital hoje!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recursos Adicionais
|
||||||
|
|
||||||
|
- [Tutorial: Como Criar um BCard](/tutoriais/tecnologia/como-criar-um-bcard)
|
||||||
|
- [BCards para Advogados](/tutoriais/advocacia/como-advogados-podem-usar-bcards)
|
||||||
|
- [Fale com nosso suporte](/support)
|
||||||
@ -0,0 +1,197 @@
|
|||||||
|
---
|
||||||
|
title: "BCards para Advogados: Guia Completo"
|
||||||
|
description: "Descubra como advogados podem usar o BCards para fortalecer sua presença digital, atrair mais clientes e organizar todos os seus contatos profissionais"
|
||||||
|
keywords: "advogado, advocacia, cartão digital, marketing jurídico, presença online, bcards"
|
||||||
|
author: "BCards"
|
||||||
|
date: 2025-11-02
|
||||||
|
lastmod: 2025-11-02
|
||||||
|
image: "/images/tutoriais/advocacia/advogados-bcards-hero.jpg"
|
||||||
|
category: "advocacia"
|
||||||
|
---
|
||||||
|
|
||||||
|
# BCards para Advogados: Guia Completo
|
||||||
|
|
||||||
|
Na era digital, ter uma presença online profissional é essencial para advogados que querem se destacar e atrair mais clientes. O BCards é a solução perfeita para centralizar todos os seus contatos e informações profissionais em um único lugar.
|
||||||
|
|
||||||
|
## Por que Advogados Precisam de um BCard?
|
||||||
|
|
||||||
|
### 1. Credibilidade Profissional
|
||||||
|
|
||||||
|
Um cartão digital transmite modernidade e profissionalismo. Seus clientes verão que você está atualizado com as tecnologias atuais.
|
||||||
|
|
||||||
|
### 2. Facilidade de Contato
|
||||||
|
|
||||||
|
Centralize todas as formas de contato em um único link:
|
||||||
|
- WhatsApp para consultas rápidas
|
||||||
|
- Email profissional
|
||||||
|
- Telefone do escritório
|
||||||
|
- Endereço com localização no mapa
|
||||||
|
- Redes sociais profissionais (LinkedIn)
|
||||||
|
|
||||||
|
### 3. Marketing Jurídico Eficiente
|
||||||
|
|
||||||
|
Divulgue seus serviços de forma ética e profissional:
|
||||||
|
- Link para artigos jurídicos que você escreve
|
||||||
|
- Vídeos educativos no YouTube
|
||||||
|
- Depoimentos de clientes satisfeitos
|
||||||
|
- Áreas de atuação
|
||||||
|
|
||||||
|
## Como Configurar seu BCard Profissional
|
||||||
|
|
||||||
|
### Passo 1: Informações Essenciais
|
||||||
|
|
||||||
|
Inclua em seu perfil:
|
||||||
|
|
||||||
|
**Informações Básicas:**
|
||||||
|
- Nome completo e OAB
|
||||||
|
- Foto profissional (de terno/blazer)
|
||||||
|
- Bio concisa e profissional
|
||||||
|
- Áreas de especialização
|
||||||
|
|
||||||
|
**Exemplo de Bio:**
|
||||||
|
> "Dr. João Silva - OAB/SP 123.456
|
||||||
|
> Advogado especialista em Direito do Consumidor
|
||||||
|
> Atuação em todo território nacional
|
||||||
|
> Mais de 15 anos de experiência"
|
||||||
|
|
||||||
|
### Passo 2: Links Estratégicos
|
||||||
|
|
||||||
|
Organize seus links por ordem de importância:
|
||||||
|
|
||||||
|
1. **WhatsApp Business** - Para consultas rápidas
|
||||||
|
2. **Agendar Consulta** - Link para sistema de agendamento
|
||||||
|
3. **Áreas de Atuação** - Página explicando seus serviços
|
||||||
|
4. **Blog Jurídico** - Artigos e conteúdo educativo
|
||||||
|
5. **LinkedIn** - Networking profissional
|
||||||
|
6. **Localização** - Google Maps do seu escritório
|
||||||
|
|
||||||
|
### Passo 3: Conteúdo Educativo
|
||||||
|
|
||||||
|
Advogados podem se destacar compartilhando conhecimento:
|
||||||
|
|
||||||
|
- Artigos sobre direitos do consumidor
|
||||||
|
- Vídeos explicando processos jurídicos
|
||||||
|
- FAQ com perguntas frequentes
|
||||||
|
- Guias práticos para clientes
|
||||||
|
|
||||||
|
## Estratégias de Marketing para Advogados
|
||||||
|
|
||||||
|
### 1. Use no Cartão de Visitas
|
||||||
|
|
||||||
|
Adicione um QR Code do seu BCard no cartão de visitas físico. Assim, as pessoas podem salvar todos os seus contatos instantaneamente.
|
||||||
|
|
||||||
|
### 2. Assinatura de Email
|
||||||
|
|
||||||
|
Inclua o link do seu BCard na assinatura de todos os emails profissionais.
|
||||||
|
|
||||||
|
**Exemplo:**
|
||||||
|
```
|
||||||
|
Dr. João Silva
|
||||||
|
Advogado - OAB/SP 123.456
|
||||||
|
📱 Todos os meus contatos: bcards.site/page/advocacia/joao-silva
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Redes Sociais
|
||||||
|
|
||||||
|
- Instagram: coloque na bio
|
||||||
|
- LinkedIn: adicione na seção "Sobre"
|
||||||
|
- Facebook: fixe uma publicação com o link
|
||||||
|
|
||||||
|
### 4. Materiais Impressos
|
||||||
|
|
||||||
|
Adicione QR Codes em:
|
||||||
|
- Folders informativos
|
||||||
|
- Cartazes do escritório
|
||||||
|
- Apresentações em palestras
|
||||||
|
- Documentos entregues a clientes
|
||||||
|
|
||||||
|
## Links Recomendados para Advogados
|
||||||
|
|
||||||
|
### Essenciais
|
||||||
|
- ✅ WhatsApp Business (atendimento)
|
||||||
|
- ✅ Email profissional
|
||||||
|
- ✅ Telefone do escritório
|
||||||
|
- ✅ Localização (Google Maps)
|
||||||
|
- ✅ LinkedIn
|
||||||
|
|
||||||
|
### Opcionais mas Recomendados
|
||||||
|
- 📝 Blog jurídico
|
||||||
|
- 📺 Canal no YouTube (vídeos educativos)
|
||||||
|
- 📅 Sistema de agendamento online
|
||||||
|
- 📄 Portfólio de casos (quando permitido)
|
||||||
|
- 💬 Depoimentos de clientes
|
||||||
|
- 📖 E-books e materiais gratuitos
|
||||||
|
|
||||||
|
## Ética e Boas Práticas
|
||||||
|
|
||||||
|
### O que Fazer ✅
|
||||||
|
|
||||||
|
- Mantenha uma apresentação sóbria e profissional
|
||||||
|
- Use foto com traje formal
|
||||||
|
- Divulgue conteúdo educativo
|
||||||
|
- Informe número da OAB sempre
|
||||||
|
- Seja transparente sobre áreas de atuação
|
||||||
|
|
||||||
|
### O que Evitar ❌
|
||||||
|
|
||||||
|
- Promessas de resultado garantido
|
||||||
|
- Comparações com outros advogados
|
||||||
|
- Preços divulgados publicamente (consulte a OAB)
|
||||||
|
- Linguagem sensacionalista
|
||||||
|
- Imagens inadequadas
|
||||||
|
|
||||||
|
## Casos de Uso Reais
|
||||||
|
|
||||||
|
### Caso 1: Dra. Maria - Direito de Família
|
||||||
|
|
||||||
|
A Dra. Maria aumentou suas consultas em 40% após criar seu BCard. Ela incluiu:
|
||||||
|
- Link para agendar consulta online
|
||||||
|
- Artigos sobre divórcio e pensão alimentícia
|
||||||
|
- Depoimentos de clientes (com autorização)
|
||||||
|
- WhatsApp exclusivo para novos casos
|
||||||
|
|
||||||
|
### Caso 2: Dr. Pedro - Direito Trabalhista
|
||||||
|
|
||||||
|
O Dr. Pedro usa seu BCard para:
|
||||||
|
- Compartilhar guias sobre direitos trabalhistas
|
||||||
|
- Vídeos curtos explicando rescisão, FGTS, etc.
|
||||||
|
- Formulário para pré-avaliação de casos
|
||||||
|
- Links para suas redes sociais educativas
|
||||||
|
|
||||||
|
## Planos Recomendados
|
||||||
|
|
||||||
|
### Para Advogados Iniciantes
|
||||||
|
|
||||||
|
**Plano Básico (R$ 12,90/mês)**
|
||||||
|
- Até 15 links
|
||||||
|
- Temas profissionais
|
||||||
|
- Analytics para acompanhar acessos
|
||||||
|
|
||||||
|
### Para Escritórios Estabelecidos
|
||||||
|
|
||||||
|
**Plano Premium (R$ 29,90/mês)**
|
||||||
|
- Links ilimitados
|
||||||
|
- Logo do escritório
|
||||||
|
- Cores personalizadas
|
||||||
|
- Suporte prioritário
|
||||||
|
- Upload de PDFs para contratos, petições e materiais exclusivos
|
||||||
|
|
||||||
|
## Conclusão
|
||||||
|
|
||||||
|
O BCards é uma ferramenta essencial para advogados modernos que querem:
|
||||||
|
- Facilitar o contato com clientes
|
||||||
|
- Apresentar-se de forma profissional
|
||||||
|
- Centralizar todas as informações em um só lugar
|
||||||
|
- Aumentar a captação de clientes
|
||||||
|
|
||||||
|
A melhor parte? É rápido de configurar e totalmente compatível com as regras da OAB para marketing jurídico.
|
||||||
|
|
||||||
|
**Pronto para modernizar sua presença digital?** [Crie seu BCard profissional agora!](https://bcards.site)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recursos Adicionais
|
||||||
|
|
||||||
|
- [Tutorial: Como Criar um BCard](/tutoriais/tecnologia/como-criar-um-bcard)
|
||||||
|
- [Suporte BCards](/support)
|
||||||
|
- [Políticas de Privacidade](/privacidade)
|
||||||
148
Content/Tutoriais/tecnologia/como-criar-um-bcard.pt-BR.md
Normal file
148
Content/Tutoriais/tecnologia/como-criar-um-bcard.pt-BR.md
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
---
|
||||||
|
title: "Como Criar um BCard Profissional em 5 Minutos"
|
||||||
|
description: "Aprenda passo a passo como criar seu cartão digital profissional com BCards e centralize todos os seus links em um único lugar"
|
||||||
|
keywords: "bcards, cartão digital, tutorial, tecnologia, links, página profissional"
|
||||||
|
author: "BCards"
|
||||||
|
date: 2025-11-02
|
||||||
|
lastmod: 2025-11-02
|
||||||
|
image: "/images/tutoriais/tecnologia/criar-bcard-hero.jpg"
|
||||||
|
category: "tecnologia"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Como Criar um BCard Profissional em 5 Minutos
|
||||||
|
|
||||||
|
Criar um cartão digital profissional nunca foi tão fácil! Neste tutorial, vamos mostrar como você pode ter sua página de links profissional em apenas 5 minutos.
|
||||||
|
|
||||||
|
## O que é um BCard?
|
||||||
|
|
||||||
|
O BCard é sua página profissional na internet, onde você centraliza todos os seus links importantes:
|
||||||
|
|
||||||
|
- Redes sociais (Instagram, LinkedIn, Facebook)
|
||||||
|
- WhatsApp para contato
|
||||||
|
- Site ou portfólio
|
||||||
|
- Links de produtos ou serviços
|
||||||
|
- E muito mais!
|
||||||
|
|
||||||
|
## Passo 1: Faça seu Cadastro
|
||||||
|
|
||||||
|
1. Acesse [bcards.site](https://bcards.site)
|
||||||
|
2. Clique em "Criar meu BCard grátis"
|
||||||
|
3. Escolha fazer login com Google ou Microsoft
|
||||||
|
4. Pronto! Sua conta está criada
|
||||||
|
|
||||||
|
> **Dica:** Use o mesmo email que você usa profissionalmente para facilitar o gerenciamento
|
||||||
|
|
||||||
|
## Passo 2: Escolha seu Tema
|
||||||
|
|
||||||
|
Temos diversos temas profissionais disponíveis:
|
||||||
|
|
||||||
|
- **Minimalista**: Clean e elegante
|
||||||
|
- **Moderno**: Cores vibrantes e design atual
|
||||||
|
- **Profissional**: Sóbrio e corporativo
|
||||||
|
- **Criativo**: Para quem quer se destacar
|
||||||
|
|
||||||
|
Escolha o tema que mais combina com sua personalidade ou marca!
|
||||||
|
|
||||||
|
## Passo 3: Adicione seus Links
|
||||||
|
|
||||||
|
Agora é hora de adicionar os links mais importantes:
|
||||||
|
|
||||||
|
1. Clique em "Adicionar Link"
|
||||||
|
2. Escolha o tipo (Instagram, WhatsApp, Site, etc.)
|
||||||
|
3. Cole o link
|
||||||
|
4. Dê um título descritivo
|
||||||
|
5. Salve!
|
||||||
|
|
||||||
|
### Tipos de Links Suportados
|
||||||
|
|
||||||
|
- Redes Sociais (Instagram, Facebook, LinkedIn, TikTok)
|
||||||
|
- Contato (WhatsApp, Email, Telefone)
|
||||||
|
- Sites e Portfólios
|
||||||
|
- Vídeos (YouTube, Vimeo)
|
||||||
|
- Documentos e PDFs
|
||||||
|
- Links personalizados
|
||||||
|
|
||||||
|
## Passo 4: Personalize seu Perfil
|
||||||
|
|
||||||
|
Deixe seu BCard com a sua cara:
|
||||||
|
|
||||||
|
- Adicione uma foto de perfil profissional
|
||||||
|
- Escreva uma bio atrativa
|
||||||
|
- Configure cores personalizadas (plano Premium)
|
||||||
|
- Adicione seu logo (plano Premium)
|
||||||
|
|
||||||
|
## Passo 5: Compartilhe!
|
||||||
|
|
||||||
|
Agora que está pronto, é hora de compartilhar:
|
||||||
|
|
||||||
|
- Seu link será: `bcards.site/page/{categoria}/{seu-nome}`
|
||||||
|
- Adicione na bio do Instagram
|
||||||
|
- Coloque na assinatura de email
|
||||||
|
- Compartilhe em grupos de WhatsApp
|
||||||
|
- Adicione ao cartão de visitas físico com QR Code
|
||||||
|
|
||||||
|
## Dicas Profissionais
|
||||||
|
|
||||||
|
### 1. Mantenha Atualizado
|
||||||
|
|
||||||
|
Revise seus links regularmente e remova os que não são mais relevantes.
|
||||||
|
|
||||||
|
### 2. Use Descrições Claras
|
||||||
|
|
||||||
|
Ao invés de "Clique aqui", use "Veja meu portfólio completo" ou "Agende uma consulta".
|
||||||
|
|
||||||
|
### 3. Priorize os Links Mais Importantes
|
||||||
|
|
||||||
|
Coloque os links mais relevantes no topo. As pessoas geralmente clicam nos primeiros links.
|
||||||
|
|
||||||
|
### 4. Teste Regularmente
|
||||||
|
|
||||||
|
Clique em todos os seus links periodicamente para garantir que estão funcionando.
|
||||||
|
|
||||||
|
## Planos e Recursos
|
||||||
|
|
||||||
|
### Plano Gratuito
|
||||||
|
|
||||||
|
- Até 5 links
|
||||||
|
- Temas básicos
|
||||||
|
- Analytics básico
|
||||||
|
|
||||||
|
### Plano Básico (R$ 12,90/mês)
|
||||||
|
|
||||||
|
- Até 15 links
|
||||||
|
- Todos os temas
|
||||||
|
- Analytics detalhado
|
||||||
|
|
||||||
|
### Plano Premium (R$ 29,90/mês)
|
||||||
|
|
||||||
|
- Links ilimitados
|
||||||
|
- Temas personalizados
|
||||||
|
- Logo personalizado
|
||||||
|
- Suporte prioritário
|
||||||
|
- Upload de PDFs para materiais extras
|
||||||
|
|
||||||
|
## Conclusão
|
||||||
|
|
||||||
|
Criar seu BCard profissional é rápido, fácil e pode transformar a forma como você se apresenta online. Em apenas 5 minutos, você tem uma página profissional que centraliza todos os seus contatos e links importantes.
|
||||||
|
|
||||||
|
**Pronto para começar?** [Crie seu BCard grátis agora!](https://bcards.site)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Perguntas Frequentes
|
||||||
|
|
||||||
|
### Posso mudar meu tema depois?
|
||||||
|
|
||||||
|
Sim! Você pode mudar o tema quantas vezes quiser, a qualquer momento.
|
||||||
|
|
||||||
|
### Como faço para adicionar um QR Code?
|
||||||
|
|
||||||
|
No dashboard, clique em "Baixar QR Code" para fazer download do código QR do seu BCard.
|
||||||
|
|
||||||
|
### Posso ter mais de um BCard?
|
||||||
|
|
||||||
|
No momento, cada conta pode ter apenas um BCard ativo.
|
||||||
|
|
||||||
|
### Precisa de mais ajuda?
|
||||||
|
|
||||||
|
Entre em contato com nosso [suporte](https://bcards.site/support) ou confira outros tutoriais!
|
||||||
10
README.md
10
README.md
@ -16,9 +16,9 @@ Um clone profissional do LinkTree desenvolvido em ASP.NET Core MVC, focado no me
|
|||||||
- **Renderização SSR**: SEO-friendly
|
- **Renderização SSR**: SEO-friendly
|
||||||
|
|
||||||
### 🎯 Planos e Pricing (Estratégia Decoy)
|
### 🎯 Planos e Pricing (Estratégia Decoy)
|
||||||
- **Básico** (R$ 9,90/mês): 5 links, temas básicos, analytics simples
|
- **Básico** (R$ 12,90/mês): 5 links, temas básicos, analytics simples
|
||||||
- **Profissional** (R$ 24,90/mês): 15 links, todos os temas, analytics avançado, domínio personalizado *(DECOY)*
|
- **Profissional** (R$ 25,90/mês): 15 links, todos os temas, analytics avançado, domínio personalizado *(DECOY)*
|
||||||
- **Premium** (R$ 29,90/mês): Links ilimitados, temas customizáveis, analytics completo, múltiplos domínios
|
- **Premium** (R$ 29,90/mês): Links ilimitados, temas customizáveis, analytics completo, múltiplos domínios, upload de PDFs
|
||||||
|
|
||||||
## 🛠️ Tecnologias
|
## 🛠️ Tecnologias
|
||||||
|
|
||||||
@ -94,8 +94,8 @@ Edite `appsettings.json` ou `appsettings.Development.json`:
|
|||||||
|
|
||||||
1. Crie uma conta no [Stripe](https://stripe.com)
|
1. Crie uma conta no [Stripe](https://stripe.com)
|
||||||
2. Configure os produtos e preços:
|
2. Configure os produtos e preços:
|
||||||
- Básico: R$ 9,90/mês
|
- Básico: R$ 12,90/mês
|
||||||
- Profissional: R$ 24,90/mês
|
- Profissional: R$ 25,90/mês
|
||||||
- Premium: R$ 29,90/mês
|
- Premium: R$ 29,90/mês
|
||||||
3. Configure webhooks para: `/webhook/stripe`
|
3. Configure webhooks para: `/webhook/stripe`
|
||||||
4. Eventos necessários:
|
4. Eventos necessários:
|
||||||
|
|||||||
1357
RELATORIO_VIABILIDADE_ARTIGOS.md
Normal file
1357
RELATORIO_VIABILIDADE_ARTIGOS.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,67 @@
|
|||||||
|
using BCards.Web.Areas.Tutoriais.Models.ViewModels;
|
||||||
|
using BCards.Web.Areas.Tutoriais.Services;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace BCards.Web.Areas.Artigos.Controllers;
|
||||||
|
|
||||||
|
[Area("Artigos")]
|
||||||
|
public class ArtigosController : Controller
|
||||||
|
{
|
||||||
|
private readonly IMarkdownService _markdownService;
|
||||||
|
private readonly ILogger<ArtigosController> _logger;
|
||||||
|
|
||||||
|
public ArtigosController(
|
||||||
|
IMarkdownService markdownService,
|
||||||
|
ILogger<ArtigosController> logger)
|
||||||
|
{
|
||||||
|
_markdownService = markdownService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /artigos
|
||||||
|
public async Task<IActionResult> Index()
|
||||||
|
{
|
||||||
|
var artigos = await _markdownService
|
||||||
|
.GetAllArticlesAsync("Artigos", "pt-BR");
|
||||||
|
|
||||||
|
return View(artigos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /artigos/{slug}
|
||||||
|
public async Task<IActionResult> Article(string slug)
|
||||||
|
{
|
||||||
|
// Sanitização
|
||||||
|
slug = slug.Replace("..", "").Replace("/", "").Replace("\\", "");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var article = await _markdownService.GetArticleAsync(
|
||||||
|
$"Artigos/{slug}",
|
||||||
|
"pt-BR"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (article == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Artigo não encontrado: {Slug}", slug);
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buscar outros artigos para "Leia também"
|
||||||
|
article.RelatedArticles = await _markdownService
|
||||||
|
.GetAllArticlesAsync("Artigos", "pt-BR");
|
||||||
|
|
||||||
|
article.RelatedArticles = article.RelatedArticles
|
||||||
|
.Where(a => a.Slug != slug)
|
||||||
|
.OrderByDescending(a => a.Date)
|
||||||
|
.Take(3)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return View(article);
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Arquivo markdown não encontrado: {Slug}", slug);
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
253
src/BCards.Web/Areas/Artigos/Views/Artigos/Article.cshtml
Normal file
253
src/BCards.Web/Areas/Artigos/Views/Artigos/Article.cshtml
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
@model BCards.Web.Areas.Tutoriais.Models.ViewModels.ArticleViewModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = Model.Metadata.Title;
|
||||||
|
}
|
||||||
|
|
||||||
|
@section Head {
|
||||||
|
<!-- Meta Tags SEO -->
|
||||||
|
<meta name="description" content="@Model.Metadata.Description">
|
||||||
|
<meta name="keywords" content="@Model.Metadata.Keywords">
|
||||||
|
<meta name="author" content="@Model.Metadata.Author">
|
||||||
|
<meta name="robots" content="index, follow">
|
||||||
|
<link rel="canonical" href="@Url.Action("Article", "Artigos", new { area = "Artigos", slug = Model.Slug }, Context.Request.Scheme)">
|
||||||
|
|
||||||
|
<!-- Open Graph -->
|
||||||
|
<meta property="og:type" content="article">
|
||||||
|
<meta property="og:title" content="@Model.Metadata.Title">
|
||||||
|
<meta property="og:description" content="@Model.Metadata.Description">
|
||||||
|
<meta property="og:image" content="@Model.Metadata.Image">
|
||||||
|
<meta property="og:url" content="@Url.Action("Article", "Artigos", new { area = "Artigos", slug = Model.Slug }, Context.Request.Scheme)">
|
||||||
|
<meta property="article:published_time" content="@Model.Metadata.Date.ToString("yyyy-MM-ddTHH:mm:ssZ")">
|
||||||
|
<meta property="article:modified_time" content="@Model.Metadata.LastMod.ToString("yyyy-MM-ddTHH:mm:ssZ")">
|
||||||
|
<meta property="article:author" content="@Model.Metadata.Author">
|
||||||
|
|
||||||
|
<!-- Twitter Card -->
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:title" content="@Model.Metadata.Title">
|
||||||
|
<meta name="twitter:description" content="@Model.Metadata.Description">
|
||||||
|
<meta name="twitter:image" content="@Model.Metadata.Image">
|
||||||
|
|
||||||
|
<!-- Schema.org JSON-LD -->
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@@context": "https://schema.org",
|
||||||
|
"@@type": "Article",
|
||||||
|
"headline": "@Model.Metadata.Title",
|
||||||
|
"description": "@Model.Metadata.Description",
|
||||||
|
"image": "@Model.Metadata.Image",
|
||||||
|
"datePublished": "@Model.Metadata.Date.ToString("yyyy-MM-dd")",
|
||||||
|
"dateModified": "@Model.Metadata.LastMod.ToString("yyyy-MM-dd")",
|
||||||
|
"author": {
|
||||||
|
"@@type": "Person",
|
||||||
|
"name": "@Model.Metadata.Author"
|
||||||
|
},
|
||||||
|
"publisher": {
|
||||||
|
"@@type": "Organization",
|
||||||
|
"name": "BCards",
|
||||||
|
"logo": {
|
||||||
|
"@@type": "ImageObject",
|
||||||
|
"url": "https://bcards.site/logo.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- BreadcrumbList Schema -->
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@@context": "https://schema.org",
|
||||||
|
"@@type": "BreadcrumbList",
|
||||||
|
"itemListElement": [
|
||||||
|
{
|
||||||
|
"@@type": "ListItem",
|
||||||
|
"position": 1,
|
||||||
|
"name": "Início",
|
||||||
|
"item": "https://bcards.site"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@@type": "ListItem",
|
||||||
|
"position": 2,
|
||||||
|
"name": "Artigos",
|
||||||
|
"item": "https://bcards.site/artigos"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@@type": "ListItem",
|
||||||
|
"position": 3,
|
||||||
|
"name": "@Model.Metadata.Title"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<!-- Breadcrumb -->
|
||||||
|
<nav aria-label="breadcrumb" class="mb-4">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="/">Início</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="@Url.Action("Index", "Artigos", new { area = "Artigos" })">Artigos</a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">@Model.Metadata.Title</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Article Header -->
|
||||||
|
<article>
|
||||||
|
<header class="mb-4">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<span class="badge bg-success me-2">✨ Artigo</span>
|
||||||
|
<span class="text-muted small"><i class="far fa-clock me-1"></i> @Model.Metadata.ReadingTimeMinutes min de leitura</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="display-5 mb-3">@Model.Metadata.Title</h1>
|
||||||
|
<p class="lead text-muted">@Model.Metadata.Description</p>
|
||||||
|
<div class="d-flex align-items-center text-muted small mb-3">
|
||||||
|
<span class="me-3"><i class="fas fa-user me-1"></i> @Model.Metadata.Author</span>
|
||||||
|
<span class="me-3"><i class="fas fa-calendar me-1"></i> @Model.Metadata.Date.ToString("dd/MM/yyyy")</span>
|
||||||
|
<span><i class="fas fa-sync me-1"></i> Atualizado em @Model.Metadata.LastMod.ToString("dd/MM/yyyy")</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(Model.Metadata.Image))
|
||||||
|
{
|
||||||
|
<img src="@Model.Metadata.Image" class="img-fluid rounded mb-4" alt="@Model.Metadata.Title">
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Article Content -->
|
||||||
|
<div class="article-content">
|
||||||
|
@Html.Raw(Model.HtmlContent)
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<!-- CTA -->
|
||||||
|
<div class="alert alert-primary mt-5" role="alert">
|
||||||
|
<h4 class="alert-heading"><i class="fas fa-rocket me-2"></i> Pronto para transformar seu negócio?</h4>
|
||||||
|
<p class="mb-3">Crie seu cartão digital profissional e comece a atrair mais clientes hoje mesmo!</p>
|
||||||
|
<a href="/" class="btn btn-primary">Criar meu BCard grátis</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="sticky-top" style="top: 20px;">
|
||||||
|
<!-- Related Articles -->
|
||||||
|
@if (Model.RelatedArticles.Any())
|
||||||
|
{
|
||||||
|
<div class="card border-0 shadow-sm mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title mb-3"><i class="fas fa-newspaper me-2"></i> Leia Também</h5>
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
@foreach (var related in Model.RelatedArticles)
|
||||||
|
{
|
||||||
|
<a href="@Url.Action("Article", "Artigos", new { area = "Artigos", slug = related.Slug })" class="list-group-item list-group-item-action border-0 px-0">
|
||||||
|
<div class="d-flex w-100 justify-content-between">
|
||||||
|
<h6 class="mb-1">@related.Title</h6>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted"><i class="far fa-clock me-1"></i> @related.ReadingTimeMinutes min</small>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Tutoriais CTA -->
|
||||||
|
<div class="card border-0 shadow-sm bg-light mb-4">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<i class="fas fa-book-open fa-3x text-primary mb-3"></i>
|
||||||
|
<h5 class="card-title">Quer aprender mais?</h5>
|
||||||
|
<p class="card-text small text-muted">Acesse nossos tutoriais práticos</p>
|
||||||
|
<a href="@Url.Action("Index", "Tutoriais", new { area = "Tutoriais" })" class="btn btn-sm btn-primary">Ver Tutoriais</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Help Card -->
|
||||||
|
<div class="card border-0 shadow-sm bg-light">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<i class="fas fa-question-circle fa-3x text-primary mb-3"></i>
|
||||||
|
<h5 class="card-title">Precisa de ajuda?</h5>
|
||||||
|
<p class="card-text small text-muted">Entre em contato com nosso suporte</p>
|
||||||
|
<a href="/Support" class="btn btn-sm btn-outline-primary">Falar com suporte</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Styles {
|
||||||
|
<style>
|
||||||
|
.article-content {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
.article-content h1,
|
||||||
|
.article-content h2,
|
||||||
|
.article-content h3,
|
||||||
|
.article-content h4,
|
||||||
|
.article-content h5,
|
||||||
|
.article-content h6 {
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.article-content h2 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
border-bottom: 2px solid #dee2e6;
|
||||||
|
}
|
||||||
|
.article-content h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
.article-content p {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
.article-content img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
.article-content ul,
|
||||||
|
.article-content ol {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding-left: 2rem;
|
||||||
|
}
|
||||||
|
.article-content li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.article-content code {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
.article-content pre {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.article-content blockquote {
|
||||||
|
border-left: 4px solid #198754;
|
||||||
|
padding-left: 1rem;
|
||||||
|
margin-left: 0;
|
||||||
|
font-style: italic;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
.article-content table {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
.article-content table th,
|
||||||
|
.article-content table td {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
.article-content table th {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
}
|
||||||
77
src/BCards.Web/Areas/Artigos/Views/Artigos/Index.cshtml
Normal file
77
src/BCards.Web/Areas/Artigos/Views/Artigos/Index.cshtml
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
@model List<BCards.Web.Areas.Tutoriais.Models.ArticleMetadata>
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Artigos BCards - Inspiração e Conhecimento";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row mb-5">
|
||||||
|
<div class="col-lg-8 mx-auto text-center">
|
||||||
|
<h1 class="display-4 mb-3">✨ Artigos BCards</h1>
|
||||||
|
<p class="lead text-muted">Insights, tendências e inspiração para transformar sua presença digital</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (Model.Any())
|
||||||
|
{
|
||||||
|
<div class="row g-4">
|
||||||
|
@foreach (var artigo in Model)
|
||||||
|
{
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card h-100 border-0 shadow-sm hover-shadow">
|
||||||
|
@if (!string.IsNullOrEmpty(artigo.Image))
|
||||||
|
{
|
||||||
|
<img src="@artigo.Image" class="card-img-top" alt="@artigo.Title" style="height: 200px; object-fit: cover;">
|
||||||
|
}
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">@artigo.Title</h5>
|
||||||
|
<p class="card-text text-muted small">@artigo.Description</p>
|
||||||
|
<div class="d-flex justify-content-between align-items-center text-muted small">
|
||||||
|
<span><i class="far fa-clock me-1"></i> @artigo.ReadingTimeMinutes min</span>
|
||||||
|
<span>@artigo.Date.ToString("dd/MM/yyyy")</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer bg-transparent border-0">
|
||||||
|
<a href="@Url.Action("Article", "Artigos", new { area = "Artigos", slug = artigo.Slug })" class="btn btn-sm btn-primary w-100">
|
||||||
|
Ler artigo <i class="fas fa-arrow-right ms-1"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="fas fa-newspaper fa-4x text-muted mb-3"></i>
|
||||||
|
<h3 class="text-muted">Nenhum artigo disponível ainda</h3>
|
||||||
|
<p class="text-muted">Em breve teremos conteúdo inspirador para você!</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- CTA -->
|
||||||
|
<div class="row mt-5">
|
||||||
|
<div class="col-lg-8 mx-auto">
|
||||||
|
<div class="card bg-primary text-white border-0 shadow">
|
||||||
|
<div class="card-body text-center p-5">
|
||||||
|
<h3 class="mb-3">Quer ver tutoriais práticos?</h3>
|
||||||
|
<p class="mb-4">Acesse nossa seção de tutoriais e aprenda passo a passo como usar o BCards</p>
|
||||||
|
<a href="@Url.Action("Index", "Tutoriais", new { area = "Tutoriais" })" class="btn btn-light btn-lg">
|
||||||
|
<i class="fas fa-book-open me-2"></i> Ver Tutoriais
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Styles {
|
||||||
|
<style>
|
||||||
|
.hover-shadow {
|
||||||
|
transition: box-shadow 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
.hover-shadow:hover {
|
||||||
|
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
}
|
||||||
3
src/BCards.Web/Areas/Artigos/Views/_ViewStart.cshtml
Normal file
3
src/BCards.Web/Areas/Artigos/Views/_ViewStart.cshtml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@{
|
||||||
|
Layout = "_Layout";
|
||||||
|
}
|
||||||
@ -0,0 +1,111 @@
|
|||||||
|
using BCards.Web.Areas.Tutoriais.Models;
|
||||||
|
using BCards.Web.Areas.Tutoriais.Services;
|
||||||
|
using BCards.Web.Repositories;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace BCards.Web.Areas.Tutoriais.Controllers;
|
||||||
|
|
||||||
|
[Area("Tutoriais")]
|
||||||
|
public class TutoriaisController : Controller
|
||||||
|
{
|
||||||
|
private readonly IMarkdownService _markdownService;
|
||||||
|
private readonly ICategoryRepository _categoryRepository;
|
||||||
|
private readonly ILogger<TutoriaisController> _logger;
|
||||||
|
|
||||||
|
public TutoriaisController(
|
||||||
|
IMarkdownService markdownService,
|
||||||
|
ICategoryRepository categoryRepository,
|
||||||
|
ILogger<TutoriaisController> logger)
|
||||||
|
{
|
||||||
|
_markdownService = markdownService;
|
||||||
|
_categoryRepository = categoryRepository;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /tutoriais
|
||||||
|
public async Task<IActionResult> Index()
|
||||||
|
{
|
||||||
|
var categories = await _categoryRepository.GetAllActiveAsync();
|
||||||
|
var tutoriaisPorCategoria = new Dictionary<string, List<ArticleMetadata>>();
|
||||||
|
|
||||||
|
foreach (var category in categories)
|
||||||
|
{
|
||||||
|
var artigos = await _markdownService
|
||||||
|
.GetArticlesByCategoryAsync(category.Slug, "pt-BR");
|
||||||
|
|
||||||
|
if (artigos.Any())
|
||||||
|
{
|
||||||
|
tutoriaisPorCategoria[category.Slug] = artigos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewBag.Categories = categories;
|
||||||
|
return View(tutoriaisPorCategoria);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /tutoriais/{categoria}
|
||||||
|
public async Task<IActionResult> Category(string categoria)
|
||||||
|
{
|
||||||
|
// Validar categoria existe
|
||||||
|
var category = await _categoryRepository.GetBySlugAsync(categoria);
|
||||||
|
if (category == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Categoria não encontrada: {Categoria}", categoria);
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var artigos = await _markdownService
|
||||||
|
.GetArticlesByCategoryAsync(categoria, "pt-BR");
|
||||||
|
|
||||||
|
ViewBag.Category = category;
|
||||||
|
return View(artigos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /tutoriais/{categoria}/{slug}
|
||||||
|
public async Task<IActionResult> Article(string categoria, string slug)
|
||||||
|
{
|
||||||
|
// Sanitização (segurança contra path traversal)
|
||||||
|
categoria = categoria.Replace("..", "").Replace("/", "").Replace("\\", "");
|
||||||
|
slug = slug.Replace("..", "").Replace("/", "").Replace("\\", "");
|
||||||
|
|
||||||
|
// Validar categoria existe
|
||||||
|
var category = await _categoryRepository.GetBySlugAsync(categoria);
|
||||||
|
if (category == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Categoria não encontrada: {Categoria}", categoria);
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var article = await _markdownService.GetArticleAsync(
|
||||||
|
$"Tutoriais/{categoria}/{slug}",
|
||||||
|
"pt-BR"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (article == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Artigo não encontrado: {Categoria}/{Slug}", categoria, slug);
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buscar artigos relacionados da mesma categoria
|
||||||
|
article.RelatedArticles = await _markdownService
|
||||||
|
.GetArticlesByCategoryAsync(categoria, "pt-BR");
|
||||||
|
|
||||||
|
// Remover o artigo atual dos relacionados
|
||||||
|
article.RelatedArticles = article.RelatedArticles
|
||||||
|
.Where(a => a.Slug != slug)
|
||||||
|
.Take(3)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
ViewBag.Category = category;
|
||||||
|
return View(article);
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Arquivo markdown não encontrado: {Categoria}/{Slug}", categoria, slug);
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/BCards.Web/Areas/Tutoriais/Models/ArticleMetadata.cs
Normal file
16
src/BCards.Web/Areas/Tutoriais/Models/ArticleMetadata.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
namespace BCards.Web.Areas.Tutoriais.Models;
|
||||||
|
|
||||||
|
public class ArticleMetadata
|
||||||
|
{
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public string Keywords { get; set; } = string.Empty;
|
||||||
|
public string Author { get; set; } = "BCards";
|
||||||
|
public DateTime Date { get; set; }
|
||||||
|
public DateTime LastMod { get; set; }
|
||||||
|
public string Image { get; set; } = string.Empty;
|
||||||
|
public string Culture { get; set; } = "pt-BR";
|
||||||
|
public string? Category { get; set; } // Apenas para tutoriais
|
||||||
|
public int ReadingTimeMinutes { get; set; }
|
||||||
|
public string Slug { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
namespace BCards.Web.Areas.Tutoriais.Models.ViewModels;
|
||||||
|
|
||||||
|
public class ArticleViewModel
|
||||||
|
{
|
||||||
|
public ArticleMetadata Metadata { get; set; } = new();
|
||||||
|
public string HtmlContent { get; set; } = string.Empty;
|
||||||
|
public string Slug { get; set; } = string.Empty;
|
||||||
|
public DateTime LastModified { get; set; }
|
||||||
|
public List<ArticleMetadata> RelatedArticles { get; set; } = new();
|
||||||
|
}
|
||||||
11
src/BCards.Web/Areas/Tutoriais/Services/IMarkdownService.cs
Normal file
11
src/BCards.Web/Areas/Tutoriais/Services/IMarkdownService.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using BCards.Web.Areas.Tutoriais.Models;
|
||||||
|
using BCards.Web.Areas.Tutoriais.Models.ViewModels;
|
||||||
|
|
||||||
|
namespace BCards.Web.Areas.Tutoriais.Services;
|
||||||
|
|
||||||
|
public interface IMarkdownService
|
||||||
|
{
|
||||||
|
Task<ArticleViewModel?> GetArticleAsync(string relativePath, string culture);
|
||||||
|
Task<List<ArticleMetadata>> GetArticlesByCategoryAsync(string category, string culture);
|
||||||
|
Task<List<ArticleMetadata>> GetAllArticlesAsync(string baseFolder, string culture);
|
||||||
|
}
|
||||||
240
src/BCards.Web/Areas/Tutoriais/Services/MarkdownService.cs
Normal file
240
src/BCards.Web/Areas/Tutoriais/Services/MarkdownService.cs
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
using BCards.Web.Areas.Tutoriais.Models;
|
||||||
|
using BCards.Web.Areas.Tutoriais.Models.ViewModels;
|
||||||
|
using Markdig;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using YamlDotNet.Serialization;
|
||||||
|
using YamlDotNet.Serialization.NamingConventions;
|
||||||
|
|
||||||
|
namespace BCards.Web.Areas.Tutoriais.Services;
|
||||||
|
|
||||||
|
public class MarkdownService : IMarkdownService
|
||||||
|
{
|
||||||
|
private readonly IMemoryCache _cache;
|
||||||
|
private readonly ILogger<MarkdownService> _logger;
|
||||||
|
private readonly string _contentBasePath;
|
||||||
|
private readonly MarkdownPipeline _markdownPipeline;
|
||||||
|
private readonly IDeserializer _yamlDeserializer;
|
||||||
|
|
||||||
|
public MarkdownService(
|
||||||
|
IMemoryCache cache,
|
||||||
|
ILogger<MarkdownService> logger,
|
||||||
|
IWebHostEnvironment environment)
|
||||||
|
{
|
||||||
|
_cache = cache;
|
||||||
|
_logger = logger;
|
||||||
|
_contentBasePath = Path.Combine(environment.ContentRootPath, "Content");
|
||||||
|
|
||||||
|
// Pipeline Markdig com extensões avançadas
|
||||||
|
_markdownPipeline = new MarkdownPipelineBuilder()
|
||||||
|
.UseAdvancedExtensions() // Tables, footnotes, etc.
|
||||||
|
.UseAutoLinks() // Auto-link URLs
|
||||||
|
.UseEmphasisExtras() // ~~strikethrough~~
|
||||||
|
.UseGenericAttributes() // {#id .class}
|
||||||
|
.DisableHtml() // Segurança: bloqueia HTML inline
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Deserializador YAML
|
||||||
|
_yamlDeserializer = new DeserializerBuilder()
|
||||||
|
.WithNamingConvention(CamelCaseNamingConvention.Instance)
|
||||||
|
.IgnoreUnmatchedProperties()
|
||||||
|
.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ArticleViewModel?> GetArticleAsync(string relativePath, string culture)
|
||||||
|
{
|
||||||
|
var cacheKey = $"Article_{relativePath}_{culture}";
|
||||||
|
|
||||||
|
// Verificar cache
|
||||||
|
if (_cache.TryGetValue(cacheKey, out ArticleViewModel? cachedArticle))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Artigo encontrado no cache: {Path}", relativePath);
|
||||||
|
return cachedArticle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construir caminho completo
|
||||||
|
var fullPath = Path.Combine(_contentBasePath, $"{relativePath}.{culture}.md");
|
||||||
|
|
||||||
|
if (!File.Exists(fullPath))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Arquivo não encontrado: {Path}", fullPath);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var content = await File.ReadAllTextAsync(fullPath);
|
||||||
|
var (metadata, markdownContent) = ExtractFrontmatter(content);
|
||||||
|
|
||||||
|
if (metadata == null)
|
||||||
|
{
|
||||||
|
_logger.LogError("Frontmatter inválido em: {Path}", fullPath);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Processar markdown → HTML
|
||||||
|
var htmlContent = Markdown.ToHtml(markdownContent, _markdownPipeline);
|
||||||
|
|
||||||
|
// Calcular tempo de leitura (200 palavras/minuto)
|
||||||
|
var wordCount = markdownContent.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
|
||||||
|
metadata.ReadingTimeMinutes = Math.Max(1, wordCount / 200);
|
||||||
|
|
||||||
|
// Extrair slug do path
|
||||||
|
metadata.Slug = Path.GetFileNameWithoutExtension(
|
||||||
|
Path.GetFileNameWithoutExtension(relativePath.Split('/').Last())
|
||||||
|
);
|
||||||
|
metadata.Culture = culture;
|
||||||
|
|
||||||
|
var article = new ArticleViewModel
|
||||||
|
{
|
||||||
|
Metadata = metadata,
|
||||||
|
HtmlContent = htmlContent,
|
||||||
|
Slug = metadata.Slug,
|
||||||
|
LastModified = File.GetLastWriteTimeUtc(fullPath)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cache por 1 hora
|
||||||
|
_cache.Set(cacheKey, article, TimeSpan.FromHours(1));
|
||||||
|
|
||||||
|
_logger.LogInformation("Artigo processado e cacheado: {Path}", relativePath);
|
||||||
|
return article;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao processar artigo: {Path}", fullPath);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<ArticleMetadata>> GetArticlesByCategoryAsync(string category, string culture)
|
||||||
|
{
|
||||||
|
var cacheKey = $"CategoryArticles_{category}_{culture}";
|
||||||
|
|
||||||
|
if (_cache.TryGetValue(cacheKey, out List<ArticleMetadata>? cached))
|
||||||
|
{
|
||||||
|
return cached ?? new List<ArticleMetadata>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var categoryPath = Path.Combine(_contentBasePath, "Tutoriais", category);
|
||||||
|
|
||||||
|
if (!Directory.Exists(categoryPath))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Diretório de categoria não encontrado: {Path}", categoryPath);
|
||||||
|
return new List<ArticleMetadata>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var articles = new List<ArticleMetadata>();
|
||||||
|
var files = Directory.GetFiles(categoryPath, $"*.{culture}.md");
|
||||||
|
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var content = await File.ReadAllTextAsync(file);
|
||||||
|
var (metadata, _) = ExtractFrontmatter(content);
|
||||||
|
|
||||||
|
if (metadata != null)
|
||||||
|
{
|
||||||
|
var slug = Path.GetFileNameWithoutExtension(
|
||||||
|
Path.GetFileNameWithoutExtension(file)
|
||||||
|
);
|
||||||
|
metadata.Slug = slug;
|
||||||
|
metadata.Culture = culture;
|
||||||
|
metadata.Category = category;
|
||||||
|
articles.Add(metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao processar arquivo: {File}", file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordenar por data (mais recentes primeiro)
|
||||||
|
articles = articles.OrderByDescending(a => a.Date).ToList();
|
||||||
|
|
||||||
|
_cache.Set(cacheKey, articles, TimeSpan.FromHours(1));
|
||||||
|
return articles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<ArticleMetadata>> GetAllArticlesAsync(string baseFolder, string culture)
|
||||||
|
{
|
||||||
|
var cacheKey = $"AllArticles_{baseFolder}_{culture}";
|
||||||
|
|
||||||
|
if (_cache.TryGetValue(cacheKey, out List<ArticleMetadata>? cached))
|
||||||
|
{
|
||||||
|
return cached ?? new List<ArticleMetadata>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var folderPath = Path.Combine(_contentBasePath, baseFolder);
|
||||||
|
|
||||||
|
if (!Directory.Exists(folderPath))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Pasta não encontrada: {Path}", folderPath);
|
||||||
|
return new List<ArticleMetadata>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var articles = new List<ArticleMetadata>();
|
||||||
|
var files = Directory.GetFiles(folderPath, $"*.{culture}.md", SearchOption.AllDirectories);
|
||||||
|
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var content = await File.ReadAllTextAsync(file);
|
||||||
|
var (metadata, _) = ExtractFrontmatter(content);
|
||||||
|
|
||||||
|
if (metadata != null)
|
||||||
|
{
|
||||||
|
var slug = Path.GetFileNameWithoutExtension(
|
||||||
|
Path.GetFileNameWithoutExtension(file)
|
||||||
|
);
|
||||||
|
metadata.Slug = slug;
|
||||||
|
metadata.Culture = culture;
|
||||||
|
articles.Add(metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao processar arquivo: {File}", file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
articles = articles.OrderByDescending(a => a.Date).ToList();
|
||||||
|
_cache.Set(cacheKey, articles, TimeSpan.FromHours(1));
|
||||||
|
return articles;
|
||||||
|
}
|
||||||
|
|
||||||
|
private (ArticleMetadata? metadata, string content) ExtractFrontmatter(string fileContent)
|
||||||
|
{
|
||||||
|
var lines = fileContent.Split('\n');
|
||||||
|
|
||||||
|
if (lines.Length < 3 || !lines[0].Trim().Equals("---"))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Frontmatter não encontrado (deve começar com ---)");
|
||||||
|
return (null, fileContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
var endIndex = Array.FindIndex(lines, 1, line => line.Trim().Equals("---"));
|
||||||
|
|
||||||
|
if (endIndex == -1)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Frontmatter mal formatado (falta --- de fechamento)");
|
||||||
|
return (null, fileContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var yamlContent = string.Join('\n', lines[1..endIndex]);
|
||||||
|
var metadata = _yamlDeserializer.Deserialize<ArticleMetadata>(yamlContent);
|
||||||
|
|
||||||
|
var markdownContent = string.Join('\n', lines[(endIndex + 1)..]);
|
||||||
|
|
||||||
|
return (metadata, markdownContent);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao deserializar YAML frontmatter");
|
||||||
|
return (null, fileContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
251
src/BCards.Web/Areas/Tutoriais/Views/Tutoriais/Article.cshtml
Normal file
251
src/BCards.Web/Areas/Tutoriais/Views/Tutoriais/Article.cshtml
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
@model BCards.Web.Areas.Tutoriais.Models.ViewModels.ArticleViewModel
|
||||||
|
@{
|
||||||
|
var category = ViewBag.Category as BCards.Web.Models.Category;
|
||||||
|
ViewData["Title"] = Model.Metadata.Title;
|
||||||
|
}
|
||||||
|
|
||||||
|
@section Head {
|
||||||
|
<!-- Meta Tags SEO -->
|
||||||
|
<meta name="description" content="@Model.Metadata.Description">
|
||||||
|
<meta name="keywords" content="@Model.Metadata.Keywords">
|
||||||
|
<meta name="author" content="@Model.Metadata.Author">
|
||||||
|
<meta name="robots" content="index, follow">
|
||||||
|
<link rel="canonical" href="@Url.Action("Article", "Tutoriais", new { area = "Tutoriais", categoria = category?.Slug, slug = Model.Slug }, Context.Request.Scheme)">
|
||||||
|
|
||||||
|
<!-- Open Graph -->
|
||||||
|
<meta property="og:type" content="article">
|
||||||
|
<meta property="og:title" content="@Model.Metadata.Title">
|
||||||
|
<meta property="og:description" content="@Model.Metadata.Description">
|
||||||
|
<meta property="og:image" content="@Model.Metadata.Image">
|
||||||
|
<meta property="og:url" content="@Url.Action("Article", "Tutoriais", new { area = "Tutoriais", categoria = category?.Slug, slug = Model.Slug }, Context.Request.Scheme)">
|
||||||
|
<meta property="article:published_time" content="@Model.Metadata.Date.ToString("yyyy-MM-ddTHH:mm:ssZ")">
|
||||||
|
<meta property="article:modified_time" content="@Model.Metadata.LastMod.ToString("yyyy-MM-ddTHH:mm:ssZ")">
|
||||||
|
<meta property="article:author" content="@Model.Metadata.Author">
|
||||||
|
|
||||||
|
<!-- Twitter Card -->
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:title" content="@Model.Metadata.Title">
|
||||||
|
<meta name="twitter:description" content="@Model.Metadata.Description">
|
||||||
|
<meta name="twitter:image" content="@Model.Metadata.Image">
|
||||||
|
|
||||||
|
<!-- Schema.org JSON-LD -->
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@@context": "https://schema.org",
|
||||||
|
"@@type": "Article",
|
||||||
|
"headline": "@Model.Metadata.Title",
|
||||||
|
"description": "@Model.Metadata.Description",
|
||||||
|
"image": "@Model.Metadata.Image",
|
||||||
|
"datePublished": "@Model.Metadata.Date.ToString("yyyy-MM-dd")",
|
||||||
|
"dateModified": "@Model.Metadata.LastMod.ToString("yyyy-MM-dd")",
|
||||||
|
"author": {
|
||||||
|
"@@type": "Person",
|
||||||
|
"name": "@Model.Metadata.Author"
|
||||||
|
},
|
||||||
|
"publisher": {
|
||||||
|
"@@type": "Organization",
|
||||||
|
"name": "BCards",
|
||||||
|
"logo": {
|
||||||
|
"@@type": "ImageObject",
|
||||||
|
"url": "https://bcards.site/logo.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- BreadcrumbList Schema -->
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@@context": "https://schema.org",
|
||||||
|
"@@type": "BreadcrumbList",
|
||||||
|
"itemListElement": [
|
||||||
|
{
|
||||||
|
"@@type": "ListItem",
|
||||||
|
"position": 1,
|
||||||
|
"name": "Início",
|
||||||
|
"item": "https://bcards.site"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@@type": "ListItem",
|
||||||
|
"position": 2,
|
||||||
|
"name": "Tutoriais",
|
||||||
|
"item": "https://bcards.site/tutoriais"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@@type": "ListItem",
|
||||||
|
"position": 3,
|
||||||
|
"name": "@category?.Name",
|
||||||
|
"item": "https://bcards.site/tutoriais/@category?.Slug"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@@type": "ListItem",
|
||||||
|
"position": 4,
|
||||||
|
"name": "@Model.Metadata.Title"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<!-- Breadcrumb -->
|
||||||
|
<nav aria-label="breadcrumb" class="mb-4">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="/">Início</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="@Url.Action("Index", "Tutoriais", new { area = "Tutoriais" })">Tutoriais</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="@Url.Action("Category", "Tutoriais", new { area = "Tutoriais", categoria = category?.Slug })">@category?.Name</a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">@Model.Metadata.Title</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Article Header -->
|
||||||
|
<article>
|
||||||
|
<header class="mb-4">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<span class="badge bg-primary me-2">@category?.Icon @category?.Name</span>
|
||||||
|
<span class="text-muted small"><i class="far fa-clock me-1"></i> @Model.Metadata.ReadingTimeMinutes min de leitura</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="display-5 mb-3">@Model.Metadata.Title</h1>
|
||||||
|
<p class="lead text-muted">@Model.Metadata.Description</p>
|
||||||
|
<div class="d-flex align-items-center text-muted small mb-3">
|
||||||
|
<span class="me-3"><i class="fas fa-user me-1"></i> @Model.Metadata.Author</span>
|
||||||
|
<span class="me-3"><i class="fas fa-calendar me-1"></i> @Model.Metadata.Date.ToString("dd/MM/yyyy")</span>
|
||||||
|
<span><i class="fas fa-sync me-1"></i> Atualizado em @Model.Metadata.LastMod.ToString("dd/MM/yyyy")</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(Model.Metadata.Image))
|
||||||
|
{
|
||||||
|
<img src="@Model.Metadata.Image" class="img-fluid rounded mb-4" alt="@Model.Metadata.Title">
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Article Content -->
|
||||||
|
<div class="article-content">
|
||||||
|
@Html.Raw(Model.HtmlContent)
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<!-- CTA -->
|
||||||
|
<div class="alert alert-primary mt-5" role="alert">
|
||||||
|
<h4 class="alert-heading"><i class="fas fa-rocket me-2"></i> Pronto para criar seu BCard?</h4>
|
||||||
|
<p class="mb-3">Agora que você aprendeu como funciona, que tal criar seu próprio cartão digital profissional?</p>
|
||||||
|
<a href="/" class="btn btn-primary">Criar meu BCard grátis</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="sticky-top" style="top: 20px;">
|
||||||
|
<!-- Related Articles -->
|
||||||
|
@if (Model.RelatedArticles.Any())
|
||||||
|
{
|
||||||
|
<div class="card border-0 shadow-sm mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title mb-3"><i class="fas fa-book-open me-2"></i> Tutoriais Relacionados</h5>
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
@foreach (var related in Model.RelatedArticles)
|
||||||
|
{
|
||||||
|
<a href="@Url.Action("Article", "Tutoriais", new { area = "Tutoriais", categoria = category?.Slug, slug = related.Slug })" class="list-group-item list-group-item-action border-0 px-0">
|
||||||
|
<div class="d-flex w-100 justify-content-between">
|
||||||
|
<h6 class="mb-1">@related.Title</h6>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted"><i class="far fa-clock me-1"></i> @related.ReadingTimeMinutes min</small>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Help Card -->
|
||||||
|
<div class="card border-0 shadow-sm bg-light">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<i class="fas fa-question-circle fa-3x text-primary mb-3"></i>
|
||||||
|
<h5 class="card-title">Precisa de ajuda?</h5>
|
||||||
|
<p class="card-text small text-muted">Entre em contato com nosso suporte se tiver dúvidas</p>
|
||||||
|
<a href="/Support" class="btn btn-sm btn-outline-primary">Falar com suporte</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Styles {
|
||||||
|
<style>
|
||||||
|
.article-content {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
.article-content h1,
|
||||||
|
.article-content h2,
|
||||||
|
.article-content h3,
|
||||||
|
.article-content h4,
|
||||||
|
.article-content h5,
|
||||||
|
.article-content h6 {
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.article-content h2 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
border-bottom: 2px solid #dee2e6;
|
||||||
|
}
|
||||||
|
.article-content h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
.article-content p {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
.article-content img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
.article-content ul,
|
||||||
|
.article-content ol {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding-left: 2rem;
|
||||||
|
}
|
||||||
|
.article-content li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.article-content code {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
.article-content pre {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.article-content blockquote {
|
||||||
|
border-left: 4px solid #0d6efd;
|
||||||
|
padding-left: 1rem;
|
||||||
|
margin-left: 0;
|
||||||
|
font-style: italic;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
.article-content table {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
.article-content table th,
|
||||||
|
.article-content table td {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
.article-content table th {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
}
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
@model List<BCards.Web.Areas.Tutoriais.Models.ArticleMetadata>
|
||||||
|
@{
|
||||||
|
var category = ViewBag.Category as BCards.Web.Models.Category;
|
||||||
|
ViewData["Title"] = $"Tutoriais de {category?.Name} - BCards";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row mb-5">
|
||||||
|
<div class="col-lg-8 mx-auto text-center">
|
||||||
|
<span class="display-1">@category?.Icon</span>
|
||||||
|
<h1 class="display-5 mb-3">Tutoriais de @category?.Name</h1>
|
||||||
|
<p class="lead text-muted">@category?.Description</p>
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb justify-content-center">
|
||||||
|
<li class="breadcrumb-item"><a href="/">Início</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="@Url.Action("Index", "Tutoriais", new { area = "Tutoriais" })">Tutoriais</a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">@category?.Name</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (Model.Any())
|
||||||
|
{
|
||||||
|
<div class="row g-4">
|
||||||
|
@foreach (var artigo in Model)
|
||||||
|
{
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card h-100 border-0 shadow-sm hover-shadow">
|
||||||
|
@if (!string.IsNullOrEmpty(artigo.Image))
|
||||||
|
{
|
||||||
|
<img src="@artigo.Image" class="card-img-top" alt="@artigo.Title" style="height: 200px; object-fit: cover;">
|
||||||
|
}
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">@artigo.Title</h5>
|
||||||
|
<p class="card-text text-muted small">@artigo.Description</p>
|
||||||
|
<div class="d-flex justify-content-between align-items-center text-muted small">
|
||||||
|
<span><i class="far fa-clock me-1"></i> @artigo.ReadingTimeMinutes min</span>
|
||||||
|
<span>@artigo.Date.ToString("dd/MM/yyyy")</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer bg-transparent border-0">
|
||||||
|
<a href="@Url.Action("Article", "Tutoriais", new { area = "Tutoriais", categoria = category?.Slug, slug = artigo.Slug })" class="btn btn-sm btn-primary w-100">
|
||||||
|
Ler tutorial <i class="fas fa-arrow-right ms-1"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="fas fa-book-open fa-4x text-muted mb-3"></i>
|
||||||
|
<h3 class="text-muted">Nenhum tutorial disponível nesta categoria</h3>
|
||||||
|
<p class="text-muted">Em breve teremos tutoriais para @category?.Name!</p>
|
||||||
|
<a href="@Url.Action("Index", "Tutoriais", new { area = "Tutoriais" })" class="btn btn-primary mt-3">
|
||||||
|
<i class="fas fa-arrow-left me-2"></i> Voltar para tutoriais
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Styles {
|
||||||
|
<style>
|
||||||
|
.hover-shadow {
|
||||||
|
transition: box-shadow 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
.hover-shadow:hover {
|
||||||
|
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
}
|
||||||
84
src/BCards.Web/Areas/Tutoriais/Views/Tutoriais/Index.cshtml
Normal file
84
src/BCards.Web/Areas/Tutoriais/Views/Tutoriais/Index.cshtml
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
@model Dictionary<string, List<BCards.Web.Areas.Tutoriais.Models.ArticleMetadata>>
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Tutoriais BCards - Aprenda a usar o BCards";
|
||||||
|
var categories = ViewBag.Categories as List<BCards.Web.Models.Category>;
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row mb-5">
|
||||||
|
<div class="col-lg-8 mx-auto text-center">
|
||||||
|
<h1 class="display-4 mb-3">📚 Tutoriais BCards</h1>
|
||||||
|
<p class="lead text-muted">Aprenda a usar o BCards e maximize seus resultados com guias práticos por categoria</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (Model.Any())
|
||||||
|
{
|
||||||
|
@foreach (var categorySlug in Model.Keys)
|
||||||
|
{
|
||||||
|
var category = categories?.FirstOrDefault(c => c.Slug == categorySlug);
|
||||||
|
var artigos = Model[categorySlug];
|
||||||
|
|
||||||
|
if (category != null && artigos.Any())
|
||||||
|
{
|
||||||
|
<div class="mb-5">
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
<span class="fs-2 me-2">@category.Icon</span>
|
||||||
|
<h2 class="mb-0">@category.Name</h2>
|
||||||
|
<a href="@Url.Action("Category", "Tutoriais", new { area = "Tutoriais", categoria = category.Slug })" class="ms-auto btn btn-sm btn-outline-primary">
|
||||||
|
Ver todos <i class="fas fa-arrow-right ms-1"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted mb-4">@category.Description</p>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
@foreach (var artigo in artigos.Take(3))
|
||||||
|
{
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card h-100 border-0 shadow-sm hover-shadow">
|
||||||
|
@if (!string.IsNullOrEmpty(artigo.Image))
|
||||||
|
{
|
||||||
|
<img src="@artigo.Image" class="card-img-top" alt="@artigo.Title" style="height: 200px; object-fit: cover;">
|
||||||
|
}
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">@artigo.Title</h5>
|
||||||
|
<p class="card-text text-muted small">@artigo.Description</p>
|
||||||
|
<div class="d-flex justify-content-between align-items-center text-muted small">
|
||||||
|
<span><i class="far fa-clock me-1"></i> @artigo.ReadingTimeMinutes min</span>
|
||||||
|
<span>@artigo.Date.ToString("dd/MM/yyyy")</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer bg-transparent border-0">
|
||||||
|
<a href="@Url.Action("Article", "Tutoriais", new { area = "Tutoriais", categoria = category.Slug, slug = artigo.Slug })" class="btn btn-sm btn-primary w-100">
|
||||||
|
Ler tutorial <i class="fas fa-arrow-right ms-1"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr class="my-5">
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="fas fa-book-open fa-4x text-muted mb-3"></i>
|
||||||
|
<h3 class="text-muted">Nenhum tutorial disponível ainda</h3>
|
||||||
|
<p class="text-muted">Em breve teremos tutoriais incríveis para você!</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Styles {
|
||||||
|
<style>
|
||||||
|
.hover-shadow {
|
||||||
|
transition: box-shadow 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
.hover-shadow:hover {
|
||||||
|
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
}
|
||||||
3
src/BCards.Web/Areas/Tutoriais/Views/_ViewStart.cshtml
Normal file
3
src/BCards.Web/Areas/Tutoriais/Views/_ViewStart.cshtml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@{
|
||||||
|
Layout = "_Layout";
|
||||||
|
}
|
||||||
@ -10,6 +10,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Markdig" Version="0.43.0" />
|
||||||
<PackageReference Include="MongoDB.Driver" Version="3.4.2" />
|
<PackageReference Include="MongoDB.Driver" Version="3.4.2" />
|
||||||
<PackageReference Include="Stripe.net" Version="48.4.0" />
|
<PackageReference Include="Stripe.net" Version="48.4.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="8.0.4" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="8.0.4" />
|
||||||
@ -31,12 +32,18 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.0" />
|
||||||
<PackageReference Include="AspNetCore.HealthChecks.MongoDb" Version="7.0.0" />
|
<PackageReference Include="AspNetCore.HealthChecks.MongoDb" Version="7.0.0" />
|
||||||
<PackageReference Include="AspNetCore.DataProtection.MongoDB" Version="8.0.0" />
|
<PackageReference Include="AspNetCore.DataProtection.MongoDB" Version="8.0.0" />
|
||||||
|
<PackageReference Include="YamlDotNet" Version="16.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="Resources\**\*.resx" />
|
<EmbeddedResource Include="Resources\**\*.resx" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Content\Artigos\" />
|
||||||
|
<Folder Include="Content\Tutoriais\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)'=='Testing'">
|
<PropertyGroup Condition="'$(Configuration)'=='Testing'">
|
||||||
<DefineConstants>$(DefineConstants);TESTING</DefineConstants>
|
<DefineConstants>$(DefineConstants);TESTING</DefineConstants>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
395
src/BCards.Web/Content/Artigos/bcards-vs-linktree.pt-BR.md
Normal file
395
src/BCards.Web/Content/Artigos/bcards-vs-linktree.pt-BR.md
Normal file
@ -0,0 +1,395 @@
|
|||||||
|
---
|
||||||
|
title: "BCards vs LinkTree: Compare e Escolha a Melhor Alternativa Brasileira"
|
||||||
|
description: "Comparação completa entre BCards e LinkTree. Descubra qual plataforma oferece melhor custo-benefício, mais funcionalidades e suporte em português para sua página de links profissional."
|
||||||
|
keywords: "linktree, alternativa ao linktree, bcards, página de links, linktree brasil, comparação linktree, melhor que linktree"
|
||||||
|
author: "Equipe BCards"
|
||||||
|
date: 2025-01-15
|
||||||
|
lastMod: 2025-01-15
|
||||||
|
image: "/images/artigos/bcards-vs-linktree.jpg"
|
||||||
|
culture: "pt-BR"
|
||||||
|
---
|
||||||
|
|
||||||
|
# BCards vs LinkTree: Compare e Escolha a Melhor Alternativa Brasileira
|
||||||
|
|
||||||
|
Se você está procurando uma alternativa ao LinkTree, provavelmente já percebeu que existem diversas opções no mercado. Mas qual delas oferece o melhor custo-benefício para profissionais e empresas brasileiras?
|
||||||
|
|
||||||
|
Neste artigo, vamos fazer uma comparação honesta e detalhada entre **BCards** e **LinkTree**, analisando funcionalidades, preços, suporte e muito mais para ajudá-lo a tomar a melhor decisão.
|
||||||
|
|
||||||
|
## O Que São Plataformas de Bio Links?
|
||||||
|
|
||||||
|
Antes de mergulharmos na comparação, vamos entender o conceito. Plataformas como LinkTree e BCards permitem que você crie uma página única com múltiplos links, perfeita para compartilhar em suas redes sociais (especialmente Instagram, TikTok e Twitter, onde você tem espaço limitado para links).
|
||||||
|
|
||||||
|
Em vez de escolher apenas um link na sua bio, você direciona seus seguidores para uma página centralizada com todos os seus links importantes: site, blog, produtos, redes sociais, portfólio e muito mais.
|
||||||
|
|
||||||
|
## Visão Geral: BCards vs LinkTree
|
||||||
|
|
||||||
|
### LinkTree: O Pioneer Global
|
||||||
|
|
||||||
|
O LinkTree foi uma das primeiras plataformas de bio links a ganhar popularidade mundial. Fundado na Austrália, hoje é usado por milhões de criadores de conteúdo, influenciadores e empresas ao redor do mundo.
|
||||||
|
|
||||||
|
**Principais Características:**
|
||||||
|
- Interface simples e intuitiva
|
||||||
|
- Grande reconhecimento de marca internacional
|
||||||
|
- Diversas integrações com plataformas globais
|
||||||
|
- Suporte em inglês
|
||||||
|
|
||||||
|
### BCards: A Alternativa Brasileira
|
||||||
|
|
||||||
|
O BCards é uma plataforma brasileira desenvolvida especificamente para atender às necessidades do mercado nacional. Criada por profissionais que entendem os desafios locais, oferece funcionalidades pensadas para o público brasileiro.
|
||||||
|
|
||||||
|
**Principais Características:**
|
||||||
|
- Totalmente em português
|
||||||
|
- Suporte local e personalizado
|
||||||
|
- Preços em reais (sem variação cambial)
|
||||||
|
- URLs organizadas por categoria profissional
|
||||||
|
- Foco no mercado brasileiro e latino-americano
|
||||||
|
|
||||||
|
## Comparação Detalhada de Funcionalidades
|
||||||
|
|
||||||
|
### 1. Estrutura de URLs
|
||||||
|
|
||||||
|
**LinkTree:**
|
||||||
|
- Formato: `linktr.ee/seuusuario`
|
||||||
|
- URL genérica para todos os usuários
|
||||||
|
|
||||||
|
**BCards:**
|
||||||
|
- Formato: `bcards.site/page/{categoria}/{seu-slug}`
|
||||||
|
- Exemplos:
|
||||||
|
- `bcards.site/page/advocacia/maria-silva`
|
||||||
|
- `bcards.site/page/tecnologia/joao-dev`
|
||||||
|
- `bcards.site/page/saude/dra-ana-cardiologista`
|
||||||
|
|
||||||
|
**Vantagem BCards:** URLs hierárquicas melhoram significativamente o SEO e a credibilidade profissional. Quando alguém visita seu link, já sabe qual é sua área de atuação antes mesmo de abrir a página.
|
||||||
|
|
||||||
|
### 2. Personalização Visual
|
||||||
|
|
||||||
|
**LinkTree:**
|
||||||
|
- Plano gratuito: Temas básicos limitados
|
||||||
|
- Planos pagos: Mais opções de personalização
|
||||||
|
- Temas pré-definidos
|
||||||
|
|
||||||
|
**BCards:**
|
||||||
|
- Plano Básico: 5+ temas profissionais
|
||||||
|
- Plano Profissional: 10+ temas premium
|
||||||
|
- Plano Premium: Temas customizáveis + editor CSS
|
||||||
|
|
||||||
|
**Vantagem BCards:** Maior flexibilidade de personalização mesmo nos planos mais acessíveis, permitindo que sua página reflita sua identidade visual.
|
||||||
|
|
||||||
|
### 3. Quantidade de Links
|
||||||
|
|
||||||
|
**LinkTree:**
|
||||||
|
- Plano gratuito: Links ilimitados
|
||||||
|
- Destaque para 1 link por vez (feature paga)
|
||||||
|
|
||||||
|
**BCards:**
|
||||||
|
- Plano Básico (R$ 12,90): 5 links
|
||||||
|
- Plano Profissional (R$ 25,90): 15 links
|
||||||
|
- Plano Premium (R$ 29,90): Links ilimitados
|
||||||
|
|
||||||
|
**Empate:** LinkTree oferece links ilimitados gratuitamente, mas limita recursos de destaque. BCards oferece estrutura mais organizada com categorias, mas limita quantidade nos planos básicos.
|
||||||
|
|
||||||
|
### 4. Analytics e Métricas
|
||||||
|
|
||||||
|
**LinkTree:**
|
||||||
|
- Plano gratuito: Métricas básicas de cliques
|
||||||
|
- Planos pagos: Analytics avançado, integração com Google Analytics, rastreamento de conversões
|
||||||
|
|
||||||
|
**BCards:**
|
||||||
|
- Todos os planos: Contadores de cliques por link
|
||||||
|
- Dashboards com métricas de desempenho
|
||||||
|
- Relatórios de visitantes
|
||||||
|
|
||||||
|
**Empate:** Ambas as plataformas oferecem analytics suficientes para a maioria dos usuários. LinkTree tem vantagem em integrações avançadas, BCards oferece simplicidade e clareza nos dados.
|
||||||
|
|
||||||
|
### 5. Integrações
|
||||||
|
|
||||||
|
**LinkTree:**
|
||||||
|
- Integrações com diversas plataformas globais
|
||||||
|
- Facebook Pixel, Google Analytics, TikTok, Spotify, Apple Music
|
||||||
|
- Mais de 100 integrações
|
||||||
|
|
||||||
|
**BCards:**
|
||||||
|
- Integrações com principais plataformas brasileiras
|
||||||
|
- Google Analytics
|
||||||
|
- Redes sociais principais (Instagram, Facebook, WhatsApp)
|
||||||
|
- Foco em ferramentas relevantes para o mercado brasileiro
|
||||||
|
|
||||||
|
**Vantagem LinkTree:** Maior quantidade de integrações com plataformas internacionais, ideal para criadores de conteúdo global.
|
||||||
|
|
||||||
|
### 6. Sistema de Moderação e Qualidade
|
||||||
|
|
||||||
|
**LinkTree:**
|
||||||
|
- Moderação automatizada
|
||||||
|
- Políticas de uso global
|
||||||
|
|
||||||
|
**BCards:**
|
||||||
|
- Sistema de moderação humanizada
|
||||||
|
- Análise de conteúdo antes da ativação
|
||||||
|
- Garantia de qualidade das páginas públicas
|
||||||
|
|
||||||
|
**Vantagem BCards:** A moderação manual garante que todas as páginas ativas mantenham um padrão de qualidade, protegendo a reputação da plataforma e dos usuários.
|
||||||
|
|
||||||
|
## Comparação de Preços (Janeiro 2025)
|
||||||
|
|
||||||
|
### LinkTree
|
||||||
|
|
||||||
|
**Free (Gratuito):**
|
||||||
|
- Links ilimitados
|
||||||
|
- Temas básicos
|
||||||
|
- Métricas limitadas
|
||||||
|
- Branding LinkTree visível
|
||||||
|
|
||||||
|
**Starter (US$ 5/mês = ~R$ 25-30/mês*):**
|
||||||
|
- Remove branding
|
||||||
|
- Mais opções de temas
|
||||||
|
- Agendamento de links
|
||||||
|
- Analytics básico
|
||||||
|
|
||||||
|
**Pro (US$ 9/mês = ~R$ 45-55/mês*):**
|
||||||
|
- Analytics avançado
|
||||||
|
- Priorização de links
|
||||||
|
- Vídeos de fundo
|
||||||
|
- Integrações avançadas
|
||||||
|
|
||||||
|
**Premium (US$ 24/mês = ~R$ 120-145/mês*):**
|
||||||
|
- Todas as features Pro
|
||||||
|
- Suporte prioritário
|
||||||
|
- Features de e-commerce
|
||||||
|
- Mais integrações
|
||||||
|
|
||||||
|
*Valores aproximados sujeitos à variação cambial
|
||||||
|
|
||||||
|
### BCards
|
||||||
|
|
||||||
|
**Básico (R$ 12,90/mês):**
|
||||||
|
- 5 links organizados
|
||||||
|
- Temas básicos
|
||||||
|
- Analytics essenciais
|
||||||
|
- URL categorizada
|
||||||
|
- Suporte em português
|
||||||
|
|
||||||
|
**Profissional (R$ 25,90/mês):**
|
||||||
|
- 15 links organizados
|
||||||
|
- Todos os temas premium
|
||||||
|
- Analytics completo
|
||||||
|
- Suporte prioritário
|
||||||
|
|
||||||
|
**Premium (R$ 29,90/mês):**
|
||||||
|
- Links ilimitados
|
||||||
|
- Temas customizáveis
|
||||||
|
- Editor CSS avançado
|
||||||
|
- Analytics detalhado
|
||||||
|
- Suporte VIP
|
||||||
|
|
||||||
|
### Análise de Custo-Benefício
|
||||||
|
|
||||||
|
Para usuários brasileiros, o BCards oferece vantagens significativas:
|
||||||
|
|
||||||
|
1. **Sem variação cambial**: Preços fixos em reais
|
||||||
|
2. **Custo inicial menor**: R$ 12,90 vs ~R$ 25-30 (Starter do LinkTree)
|
||||||
|
3. **Plano Premium mais acessível**: R$ 29,90 vs ~R$ 120-145 (Premium do LinkTree)
|
||||||
|
4. **Suporte em português**: Sem barreira linguística
|
||||||
|
|
||||||
|
**Exemplo prático:**
|
||||||
|
|
||||||
|
Um advogado que precisa de uma página profissional com 10 links:
|
||||||
|
- **LinkTree Pro**: ~R$ 45-55/mês (R$ 540-660/ano)
|
||||||
|
- **BCards Profissional**: R$ 25,90/mês (R$ 310,80/ano)
|
||||||
|
- **Economia**: R$ 241-361/ano (44-55% de economia)
|
||||||
|
|
||||||
|
## Quando Escolher LinkTree?
|
||||||
|
|
||||||
|
O LinkTree pode ser a melhor opção se você:
|
||||||
|
|
||||||
|
1. **Precisa de visibilidade internacional**: Marca reconhecida globalmente
|
||||||
|
2. **Cria conteúdo em inglês**: Audiência internacional
|
||||||
|
3. **Necessita de integrações específicas**: Plataformas não populares no Brasil
|
||||||
|
4. **Quer começar gratuitamente**: Plano free com links ilimitados
|
||||||
|
5. **Trabalha com e-commerce global**: Integrações avançadas de vendas
|
||||||
|
|
||||||
|
## Quando Escolher BCards?
|
||||||
|
|
||||||
|
O BCards é ideal se você:
|
||||||
|
|
||||||
|
1. **Atua no mercado brasileiro**: Profissionais liberais, empresas locais
|
||||||
|
2. **Valoriza suporte em português**: Comunicação clara e rápida
|
||||||
|
3. **Busca melhor custo-benefício**: Preços competitivos em reais
|
||||||
|
4. **Quer URLs profissionais**: Estrutura categorizada melhora SEO
|
||||||
|
5. **Precisa de personalização**: Temas customizáveis por preço acessível
|
||||||
|
6. **Valoriza qualidade**: Sistema de moderação garante padrão elevado
|
||||||
|
|
||||||
|
## Casos de Uso Práticos
|
||||||
|
|
||||||
|
### Caso 1: Advogada Especializada em Direito de Família
|
||||||
|
|
||||||
|
**Escolha: BCards**
|
||||||
|
|
||||||
|
Maria Silva, advogada em São Paulo, escolheu BCards porque:
|
||||||
|
- URL profissional: `bcards.site/page/advocacia/maria-silva-direito-familia`
|
||||||
|
- Preço fixo em reais (sem surpresas)
|
||||||
|
- Suporte em português para ajustar sua página
|
||||||
|
- Sistema de moderação garante credibilidade profissional
|
||||||
|
|
||||||
|
**Resultado:** Aumento de 40% em consultas via página de bio nos primeiros 3 meses.
|
||||||
|
|
||||||
|
### Caso 2: Influenciador de Tecnologia Global
|
||||||
|
|
||||||
|
**Escolha: LinkTree**
|
||||||
|
|
||||||
|
João Tech, criador de conteúdo com audiência internacional, escolheu LinkTree porque:
|
||||||
|
- Marca reconhecida globalmente
|
||||||
|
- Integrações com Patreon, Ko-fi, e outras plataformas internacionais
|
||||||
|
- Audiência em múltiplos países
|
||||||
|
- Necessidade de features de e-commerce global
|
||||||
|
|
||||||
|
**Resultado:** Facilidade para monetização internacional e reconhecimento da marca LinkTree entre seguidores estrangeiros.
|
||||||
|
|
||||||
|
### Caso 3: Personal Trainer Local
|
||||||
|
|
||||||
|
**Escolha: BCards**
|
||||||
|
|
||||||
|
Ana Fitness, personal trainer em Belo Horizonte, escolheu BCards porque:
|
||||||
|
- Atende apenas clientes locais
|
||||||
|
- Precisava de página profissional sem custo alto
|
||||||
|
- URL categorizada: `bcards.site/page/saude/ana-personal-trainer-bh`
|
||||||
|
- Todos os clientes falam português
|
||||||
|
|
||||||
|
**Resultado:** Redução de 60% no custo mensal comparado ao LinkTree Pro, mantendo todas as funcionalidades necessárias.
|
||||||
|
|
||||||
|
## Fatores Técnicos: SEO e Performance
|
||||||
|
|
||||||
|
### SEO (Otimização para Motores de Busca)
|
||||||
|
|
||||||
|
**BCards vantagens:**
|
||||||
|
- URLs hierárquicas descritivas
|
||||||
|
- Estrutura de categorias melhora indexação
|
||||||
|
- Conteúdo em português nativo
|
||||||
|
- Menor concorrência em buscas locais
|
||||||
|
|
||||||
|
**LinkTree vantagens:**
|
||||||
|
- Autoridade de domínio global mais alta
|
||||||
|
- Maior reconhecimento de marca
|
||||||
|
- Backlinks naturais de usuários internacionais
|
||||||
|
|
||||||
|
### Performance e Velocidade
|
||||||
|
|
||||||
|
Ambas as plataformas oferecem:
|
||||||
|
- Carregamento rápido (< 2 segundos)
|
||||||
|
- Responsividade mobile
|
||||||
|
- Uptime confiável (99%+)
|
||||||
|
|
||||||
|
## Suporte ao Cliente
|
||||||
|
|
||||||
|
### LinkTree
|
||||||
|
- Suporte em inglês
|
||||||
|
- Base de conhecimento extensa
|
||||||
|
- Comunidade global ativa
|
||||||
|
- Suporte prioritário apenas em planos premium
|
||||||
|
|
||||||
|
### BCards
|
||||||
|
- Suporte em português
|
||||||
|
- Atendimento personalizado
|
||||||
|
- Suporte prioritário desde plano Profissional
|
||||||
|
- Compreensão do contexto local brasileiro
|
||||||
|
|
||||||
|
**Vantagem BCards:** Para usuários que não dominam inglês ou preferem suporte local, o BCards oferece experiência significativamente melhor.
|
||||||
|
|
||||||
|
## Segurança e Privacidade
|
||||||
|
|
||||||
|
### LinkTree
|
||||||
|
- Certificado SSL
|
||||||
|
- Conformidade com GDPR (Europa)
|
||||||
|
- Políticas de privacidade internacionais
|
||||||
|
|
||||||
|
### BCards
|
||||||
|
- Certificado SSL
|
||||||
|
- Políticas alinhadas com LGPD (Brasil)
|
||||||
|
- Dados hospedados no Brasil
|
||||||
|
|
||||||
|
**Vantagem BCards:** Para empresas que precisam estar em conformidade com LGPD, ter dados hospedados no Brasil pode ser uma vantagem regulatória.
|
||||||
|
|
||||||
|
## Limitações de Cada Plataforma
|
||||||
|
|
||||||
|
### LinkTree Limitações
|
||||||
|
- Custos em dólar (variação cambial)
|
||||||
|
- Suporte não é em português
|
||||||
|
- Alguns recursos avançados são muito caros
|
||||||
|
- Foco global pode não atender necessidades locais
|
||||||
|
|
||||||
|
### BCards Limitações
|
||||||
|
- Menor reconhecimento internacional
|
||||||
|
- Menos integrações com plataformas globais
|
||||||
|
- Comunidade menor (plataforma mais nova)
|
||||||
|
- Não tem plano gratuito com links ilimitados
|
||||||
|
|
||||||
|
## Tabela Comparativa Resumida
|
||||||
|
|
||||||
|
| Característica | LinkTree | BCards |
|
||||||
|
|---------------|----------|---------|
|
||||||
|
| **Preço inicial** | Gratuito (limitado) | R$ 12,90/mês |
|
||||||
|
| **Plano Pro** | ~R$ 45-55/mês | R$ 25,90/mês |
|
||||||
|
| **Plano Premium** | ~R$ 120-145/mês | R$ 29,90/mês |
|
||||||
|
| **Moeda** | Dólar (USD) | Real (BRL) |
|
||||||
|
| **Links ilimitados** | Grátis | R$ 29,90/mês |
|
||||||
|
| **Suporte** | Inglês | Português |
|
||||||
|
| **URL** | linktr.ee/usuario | bcards.site/page/categoria/usuario |
|
||||||
|
| **Moderação** | Automatizada | Humanizada |
|
||||||
|
| **Foco** | Global | Brasil/América Latina |
|
||||||
|
| **Integrações** | 100+ | Principais |
|
||||||
|
| **Personalização** | Boa (paga) | Excelente (todos planos) |
|
||||||
|
| **SEO** | Autoridade global | URLs categorizadas |
|
||||||
|
|
||||||
|
## Migração: Como Trocar do LinkTree para BCards
|
||||||
|
|
||||||
|
Se você já usa LinkTree e está considerando migrar para BCards, o processo é simples:
|
||||||
|
|
||||||
|
1. **Exporte seus links**: Copie títulos e URLs
|
||||||
|
2. **Crie conta no BCards**: Escolha sua categoria profissional
|
||||||
|
3. **Configure sua página**: Adicione links, escolha tema
|
||||||
|
4. **Submeta para moderação**: Aguarde aprovação (24-48h)
|
||||||
|
5. **Atualize suas redes sociais**: Troque o link da bio
|
||||||
|
|
||||||
|
**Dica:** Mantenha ambas as páginas ativas durante 1-2 semanas de transição para garantir que todos os seguidores vejam o novo link.
|
||||||
|
|
||||||
|
## Conclusão: Qual Escolher?
|
||||||
|
|
||||||
|
Não existe resposta única. A melhor escolha depende do seu contexto:
|
||||||
|
|
||||||
|
**Escolha LinkTree se:**
|
||||||
|
- Você tem audiência internacional significativa
|
||||||
|
- Cria conteúdo em inglês
|
||||||
|
- Necessita de integrações específicas globais
|
||||||
|
- Quer começar gratuitamente (com limitações)
|
||||||
|
- Reconhecimento de marca internacional é importante
|
||||||
|
|
||||||
|
**Escolha BCards se:**
|
||||||
|
- Você atua principalmente no Brasil
|
||||||
|
- Valoriza suporte em português
|
||||||
|
- Busca melhor custo-benefício
|
||||||
|
- Quer URL profissional categorizada
|
||||||
|
- Prefere moderação humanizada
|
||||||
|
- Deseja evitar variação cambial
|
||||||
|
|
||||||
|
Para a maioria dos **profissionais brasileiros, pequenas empresas e criadores de conteúdo local**, o **BCards oferece melhor custo-benefício, suporte mais personalizado e funcionalidades adequadas às necessidades do mercado nacional**.
|
||||||
|
|
||||||
|
Para **criadores de conteúdo global, influenciadores internacionais e empresas que atuam em múltiplos países**, o **LinkTree pode ser a escolha mais adequada** devido ao reconhecimento internacional da marca.
|
||||||
|
|
||||||
|
## Próximos Passos
|
||||||
|
|
||||||
|
Pronto para criar sua página de links profissional?
|
||||||
|
|
||||||
|
1. **Defina suas necessidades**: Quantos links? Qual seu público?
|
||||||
|
2. **Avalie seu orçamento**: Preço fixo ou variável?
|
||||||
|
3. **Considere suporte**: Português ou inglês?
|
||||||
|
4. **Teste a plataforma**: Crie uma página e veja qual interface prefere
|
||||||
|
5. **Decida e comece**: Ambas são boas opções, escolha a melhor para você
|
||||||
|
|
||||||
|
**Experimente BCards gratuitamente** - [Criar conta agora](https://bcards.site/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Última atualização:** Janeiro 2025
|
||||||
|
|
||||||
|
*Este artigo é baseado em informações públicas das plataformas e nossa análise independente. Os preços podem variar. Consulte os sites oficiais para informações atualizadas.*
|
||||||
|
|
||||||
|
*Disclaimer: Somos a equipe BCards, mas nos esforçamos para apresentar uma comparação honesta e imparcial. Apresentamos vantagens e limitações de ambas as plataformas para ajudá-lo a tomar a melhor decisão para seu caso específico.*
|
||||||
@ -0,0 +1,339 @@
|
|||||||
|
---
|
||||||
|
title: "Transformação Digital para Pequenos Negócios: Comece Hoje Mesmo"
|
||||||
|
description: "Guia prático sobre como pequenos negócios podem iniciar sua jornada de transformação digital sem grandes investimentos. Aprenda estratégias simples e eficazes."
|
||||||
|
keywords: "transformação digital, pequenos negócios, digitalização, presença online, marketing digital"
|
||||||
|
author: "Equipe BCards"
|
||||||
|
date: 2025-01-10
|
||||||
|
lastMod: 2025-01-10
|
||||||
|
image: "/images/artigos/transformacao-digital.jpg"
|
||||||
|
culture: "pt-BR"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Transformação Digital para Pequenos Negócios: Comece Hoje Mesmo
|
||||||
|
|
||||||
|
A transformação digital não é mais um luxo reservado apenas para grandes empresas. Hoje, pequenos negócios podem (e devem) aproveitar as ferramentas digitais para crescer, alcançar novos clientes e competir no mercado moderno.
|
||||||
|
|
||||||
|
Neste artigo, você vai descobrir como iniciar sua jornada digital com investimento mínimo e resultados máximos.
|
||||||
|
|
||||||
|
## O Que É Transformação Digital?
|
||||||
|
|
||||||
|
Transformação digital é o processo de integrar tecnologias digitais em todas as áreas do seu negócio, mudando fundamentalmente como você opera e entrega valor aos clientes.
|
||||||
|
|
||||||
|
Para pequenos negócios, isso não significa gastar milhões em sistemas complexos. Significa:
|
||||||
|
- Ter presença online profissional
|
||||||
|
- Facilitar o contato com clientes
|
||||||
|
- Organizar informações de forma acessível
|
||||||
|
- Automatizar processos simples
|
||||||
|
- Usar dados para tomar decisões melhores
|
||||||
|
|
||||||
|
## Por Que Pequenos Negócios Precisam se Digitalizar?
|
||||||
|
|
||||||
|
### 1. Seus Clientes Estão Online
|
||||||
|
|
||||||
|
Mais de 70% dos brasileiros usam internet diariamente. Quando precisam de um produto ou serviço, a primeira ação é pesquisar online. Se você não está lá, está invisível.
|
||||||
|
|
||||||
|
### 2. Competitividade
|
||||||
|
|
||||||
|
Seus concorrentes já estão se digitalizando. Ficar de fora significa perder espaço no mercado.
|
||||||
|
|
||||||
|
### 3. Redução de Custos
|
||||||
|
|
||||||
|
Ferramentas digitais frequentemente custam menos que métodos tradicionais e alcançam mais pessoas.
|
||||||
|
|
||||||
|
### 4. Melhor Experiência do Cliente
|
||||||
|
|
||||||
|
Clientes valorizam conveniência: encontrar informações rapidamente, entrar em contato facilmente, agendar serviços online.
|
||||||
|
|
||||||
|
## Os 5 Pilares da Transformação Digital para Pequenos Negócios
|
||||||
|
|
||||||
|
### 1. Presença Online Profissional
|
||||||
|
|
||||||
|
**Problema:** Você não tem site, ou seu site está desatualizado.
|
||||||
|
|
||||||
|
**Solução Simples:**
|
||||||
|
- Crie uma página profissional de links (bio link)
|
||||||
|
- Organize todos seus canais em um único lugar
|
||||||
|
- Facilite o acesso a WhatsApp, Instagram, serviços
|
||||||
|
|
||||||
|
**Custo:** A partir de R$ 12,90/mês
|
||||||
|
|
||||||
|
**Resultado:** Clientes encontram suas informações facilmente, você parece mais profissional.
|
||||||
|
|
||||||
|
### 2. Relacionamento com Clientes
|
||||||
|
|
||||||
|
**Problema:** Dificuldade em manter contato constante com clientes.
|
||||||
|
|
||||||
|
**Solução Simples:**
|
||||||
|
- Use WhatsApp Business (gratuito)
|
||||||
|
- Organize contatos com etiquetas
|
||||||
|
- Configure mensagens automáticas
|
||||||
|
- Crie catálogo de produtos
|
||||||
|
|
||||||
|
**Custo:** Gratuito
|
||||||
|
|
||||||
|
**Resultado:** Comunicação mais profissional e organizada.
|
||||||
|
|
||||||
|
### 3. Divulgação Estratégica
|
||||||
|
|
||||||
|
**Problema:** Propaganda cara e ineficiente.
|
||||||
|
|
||||||
|
**Solução Simples:**
|
||||||
|
- Crie perfis profissionais em redes sociais
|
||||||
|
- Poste conteúdo relevante regularmente
|
||||||
|
- Use Instagram e Facebook Ads (começando com R$ 5/dia)
|
||||||
|
- Peça avaliações no Google Meu Negócio
|
||||||
|
|
||||||
|
**Custo:** Pode começar com R$ 0 (orgânico) ou R$ 150/mês (anúncios básicos)
|
||||||
|
|
||||||
|
**Resultado:** Mais visibilidade, novos clientes, crescimento constante.
|
||||||
|
|
||||||
|
### 4. Gestão e Organização
|
||||||
|
|
||||||
|
**Problema:** Controle manual de vendas, estoque, finanças.
|
||||||
|
|
||||||
|
**Solução Simples:**
|
||||||
|
- Use planilhas Google (gratuito)
|
||||||
|
- Adote um sistema de gestão simples (muitos têm versão gratuita)
|
||||||
|
- Organize documentos na nuvem (Google Drive/Dropbox)
|
||||||
|
|
||||||
|
**Custo:** Gratuito a R$ 30/mês
|
||||||
|
|
||||||
|
**Resultado:** Menos tempo perdido, mais controle, decisões baseadas em dados.
|
||||||
|
|
||||||
|
### 5. Pagamentos Digitais
|
||||||
|
|
||||||
|
**Problema:** Perder vendas por aceitar apenas dinheiro.
|
||||||
|
|
||||||
|
**Solução Simples:**
|
||||||
|
- Aceite Pix (gratuito)
|
||||||
|
- Use maquininha de cartão mobile
|
||||||
|
- Crie links de pagamento online
|
||||||
|
|
||||||
|
**Custo:** Taxas apenas sobre vendas (2-5%)
|
||||||
|
|
||||||
|
**Resultado:** Venda mais, facilite a vida dos clientes, profissionalize seu negócio.
|
||||||
|
|
||||||
|
## Passo a Passo: Comece Sua Transformação Digital Hoje
|
||||||
|
|
||||||
|
### Semana 1: Presença Online Básica
|
||||||
|
|
||||||
|
**Dia 1-2: Configure WhatsApp Business**
|
||||||
|
- Baixe o app WhatsApp Business
|
||||||
|
- Configure perfil profissional com horário de atendimento
|
||||||
|
- Crie mensagens automáticas
|
||||||
|
- Adicione catálogo de produtos/serviços
|
||||||
|
|
||||||
|
**Dia 3-4: Crie Perfis Profissionais**
|
||||||
|
- Instagram Business
|
||||||
|
- Facebook Page
|
||||||
|
- Google Meu Negócio
|
||||||
|
|
||||||
|
**Dia 5-7: Organize Seus Links**
|
||||||
|
- Crie página profissional de links (ex: BCards)
|
||||||
|
- Adicione todos os canais de contato
|
||||||
|
- Coloque o link na bio de todas as redes sociais
|
||||||
|
|
||||||
|
### Semana 2: Conteúdo e Engajamento
|
||||||
|
|
||||||
|
**Dias 8-10: Planeje Conteúdo**
|
||||||
|
- Defina 3 tipos de conteúdo para postar
|
||||||
|
- Exemplos: Dicas, bastidores, promoções
|
||||||
|
- Crie calendário simples (3 posts por semana)
|
||||||
|
|
||||||
|
**Dias 11-14: Comece a Postar**
|
||||||
|
- Publique primeiro conteúdo
|
||||||
|
- Responda todos os comentários
|
||||||
|
- Peça para amigos e clientes seguirem
|
||||||
|
|
||||||
|
### Semana 3: Organização Interna
|
||||||
|
|
||||||
|
**Dias 15-17: Digitalize Processos**
|
||||||
|
- Crie planilha de vendas no Google Sheets
|
||||||
|
- Liste todos os clientes em planilha de contatos
|
||||||
|
- Configure backup de fotos importantes na nuvem
|
||||||
|
|
||||||
|
**Dias 18-21: Configure Pagamentos**
|
||||||
|
- Cadastre Pix
|
||||||
|
- Avalie opções de maquininha mobile
|
||||||
|
- Crie links de pagamento
|
||||||
|
|
||||||
|
### Semana 4: Primeiras Campanhas
|
||||||
|
|
||||||
|
**Dias 22-25: Marketing Digital Básico**
|
||||||
|
- Poste sobre promoção especial
|
||||||
|
- Impulsione post no Instagram/Facebook (R$ 20)
|
||||||
|
- Monitore resultados
|
||||||
|
|
||||||
|
**Dias 26-30: Analise e Ajuste**
|
||||||
|
- Veja quais posts tiveram mais engajamento
|
||||||
|
- Identifique de onde vieram novos clientes
|
||||||
|
- Planeje próximo mês
|
||||||
|
|
||||||
|
## Ferramentas Essenciais (Maioria Gratuitas)
|
||||||
|
|
||||||
|
### Comunicação
|
||||||
|
- **WhatsApp Business**: Grátis
|
||||||
|
- **Google Workspace** (e-mail profissional): R$ 12/usuário/mês
|
||||||
|
|
||||||
|
### Organização
|
||||||
|
- **Google Drive**: 15GB grátis
|
||||||
|
- **Trello** (gestão de tarefas): Grátis
|
||||||
|
- **Google Sheets**: Grátis
|
||||||
|
|
||||||
|
### Presença Online
|
||||||
|
- **BCards** (página de links): A partir de R$ 12,90/mês
|
||||||
|
- **Instagram/Facebook**: Grátis
|
||||||
|
- **Google Meu Negócio**: Grátis
|
||||||
|
|
||||||
|
### Marketing
|
||||||
|
- **Canva** (design): Plano gratuito robusto
|
||||||
|
- **Meta Business Suite**: Grátis
|
||||||
|
- **Google Analytics**: Grátis
|
||||||
|
|
||||||
|
### Pagamentos
|
||||||
|
- **Pix**: Grátis
|
||||||
|
- **Mercado Pago**: Taxas sobre vendas
|
||||||
|
- **PagSeguro**: Taxas sobre vendas
|
||||||
|
|
||||||
|
**Investimento total inicial:** R$ 0 a R$ 50/mês
|
||||||
|
|
||||||
|
## Erros Comuns a Evitar
|
||||||
|
|
||||||
|
### 1. Tentar Fazer Tudo de Uma Vez
|
||||||
|
**Erro:** Criar 10 perfis, começar blog, loja virtual, tudo junto.
|
||||||
|
**Solução:** Comece com o básico, domine, depois expanda.
|
||||||
|
|
||||||
|
### 2. Não Ter Consistência
|
||||||
|
**Erro:** Postar muito uma semana, depois sumir por meses.
|
||||||
|
**Solução:** Defina frequência realista e mantenha.
|
||||||
|
|
||||||
|
### 3. Ignorar Clientes Online
|
||||||
|
**Erro:** Não responder comentários e mensagens rapidamente.
|
||||||
|
**Solução:** Defina horários para checar e responder (3x ao dia).
|
||||||
|
|
||||||
|
### 4. Não Mensurar Resultados
|
||||||
|
**Erro:** Investir sem saber o que funciona.
|
||||||
|
**Solução:** Acompanhe métricas básicas (seguidores, curtidas, vendas).
|
||||||
|
|
||||||
|
### 5. Copiar Concorrentes sem Personalidade
|
||||||
|
**Erro:** Ser uma cópia genérica.
|
||||||
|
**Solução:** Mostre sua personalidade, conte sua história única.
|
||||||
|
|
||||||
|
## Casos de Sucesso Reais
|
||||||
|
|
||||||
|
### Caso 1: Doceria da Dona Maria
|
||||||
|
|
||||||
|
**Antes:**
|
||||||
|
- Vendia apenas para vizinhos
|
||||||
|
- Divulgação boca a boca
|
||||||
|
- Faturamento: R$ 2.000/mês
|
||||||
|
|
||||||
|
**Ações:**
|
||||||
|
- Criou Instagram com fotos profissionais dos doces
|
||||||
|
- Configurou WhatsApp Business com catálogo
|
||||||
|
- Começou a aceitar encomendas por mensagem
|
||||||
|
- Criou página de links para facilitar contato
|
||||||
|
|
||||||
|
**Depois (6 meses):**
|
||||||
|
- Alcance em 3 bairros diferentes
|
||||||
|
- 1.200 seguidores no Instagram
|
||||||
|
- Faturamento: R$ 6.500/mês (+225%)
|
||||||
|
|
||||||
|
**Investimento:** R$ 30/mês (internet + ferramentas)
|
||||||
|
|
||||||
|
### Caso 2: Oficina do João
|
||||||
|
|
||||||
|
**Antes:**
|
||||||
|
- Clientes apenas por indicação
|
||||||
|
- Sem presença online
|
||||||
|
- Dificuldade em mostrar serviços
|
||||||
|
|
||||||
|
**Ações:**
|
||||||
|
- Criou Google Meu Negócio
|
||||||
|
- Pediu avaliações dos clientes satisfeitos
|
||||||
|
- Criou página de links com serviços e preços
|
||||||
|
- Postou fotos de antes/depois dos carros
|
||||||
|
|
||||||
|
**Depois (4 meses):**
|
||||||
|
- 5-7 novos clientes por mês via Google
|
||||||
|
- 4.8 estrelas no Google (15 avaliações)
|
||||||
|
- Aumento de 40% no faturamento
|
||||||
|
|
||||||
|
**Investimento:** R$ 0 (todas ferramentas gratuitas)
|
||||||
|
|
||||||
|
## Próximos Passos Após os Primeiros 30 Dias
|
||||||
|
|
||||||
|
### Mês 2-3: Consolidação
|
||||||
|
- Refine processos que funcionaram
|
||||||
|
- Descarte o que não deu resultado
|
||||||
|
- Aumente frequência de posts
|
||||||
|
- Comece a investir pequenas quantias em anúncios
|
||||||
|
|
||||||
|
### Mês 4-6: Expansão
|
||||||
|
- Considere criar site próprio
|
||||||
|
- Expanda para novas plataformas (TikTok, LinkedIn)
|
||||||
|
- Crie programa de fidelidade digital
|
||||||
|
- Automatize mais processos
|
||||||
|
|
||||||
|
### Mês 7-12: Maturidade
|
||||||
|
- Analise dados para decisões estratégicas
|
||||||
|
- Invista em ferramentas mais robustas
|
||||||
|
- Contrate especialistas para áreas específicas
|
||||||
|
- Expanda equipe digital se necessário
|
||||||
|
|
||||||
|
## Checklist da Transformação Digital
|
||||||
|
|
||||||
|
Use esta checklist para acompanhar seu progresso:
|
||||||
|
|
||||||
|
**Fundamentos (Primeiras 2 semanas):**
|
||||||
|
- [ ] WhatsApp Business configurado
|
||||||
|
- [ ] Instagram Business criado
|
||||||
|
- [ ] Facebook Page ativa
|
||||||
|
- [ ] Google Meu Negócio cadastrado
|
||||||
|
- [ ] Página de links profissional criada
|
||||||
|
- [ ] Todos os links atualizados nas redes sociais
|
||||||
|
|
||||||
|
**Conteúdo (Primeiro mês):**
|
||||||
|
- [ ] Calendário de conteúdo criado
|
||||||
|
- [ ] Pelo menos 12 posts publicados
|
||||||
|
- [ ] Todas as mensagens respondidas em até 24h
|
||||||
|
- [ ] Primeiras avaliações recebidas
|
||||||
|
|
||||||
|
**Organização (Primeiros 2 meses):**
|
||||||
|
- [ ] Planilha de vendas funcionando
|
||||||
|
- [ ] Backup de arquivos na nuvem
|
||||||
|
- [ ] Processos principais documentados
|
||||||
|
- [ ] Sistema de pagamento digital ativo
|
||||||
|
|
||||||
|
**Marketing (Primeiros 3 meses):**
|
||||||
|
- [ ] Pelo menos 1 campanha paga testada
|
||||||
|
- [ ] Análise de métricas semanalmente
|
||||||
|
- [ ] Estratégia de conteúdo refinada
|
||||||
|
- [ ] Base de clientes digitais crescendo
|
||||||
|
|
||||||
|
## Conclusão: O Momento É Agora
|
||||||
|
|
||||||
|
A transformação digital não é sobre tecnologia complicada ou investimentos massivos. É sobre adaptar seu negócio para o mundo moderno, onde clientes esperam:
|
||||||
|
|
||||||
|
- **Encontrar você facilmente online**
|
||||||
|
- **Entrar em contato rapidamente**
|
||||||
|
- **Ver seu trabalho/produtos**
|
||||||
|
- **Fazer negócio de forma conveniente**
|
||||||
|
|
||||||
|
Você não precisa fazer tudo perfeitamente desde o início. Precisa começar.
|
||||||
|
|
||||||
|
Cada pequeno passo digital é um passo na direção certa:
|
||||||
|
- Primeira postagem no Instagram
|
||||||
|
- Primeiro cliente que te encontrou online
|
||||||
|
- Primeira venda via mensagem
|
||||||
|
- Primeira avaliação positiva
|
||||||
|
|
||||||
|
Esses pequenos passos se acumulam. Em 6 meses, você olhará para trás e ficará surpreso com a transformação.
|
||||||
|
|
||||||
|
**Comece hoje. Seu futuro digital está a um clique de distância.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Sobre o BCards:** Ajudamos pequenos negócios e profissionais a terem presença online profissional de forma simples e acessível. Crie sua página de links agora mesmo.
|
||||||
|
|
||||||
|
[Começar minha transformação digital →](https://bcards.site/)
|
||||||
@ -0,0 +1,435 @@
|
|||||||
|
---
|
||||||
|
title: "BCards para Advogados: Guia Completo de Marketing Digital Ético"
|
||||||
|
description: "Como advogados podem usar o BCards de forma profissional e ética, respeitando as normas da OAB sobre publicidade jurídica digital."
|
||||||
|
keywords: "advogados, marketing jurídico, publicidade advocacia, OAB, marketing digital advogados"
|
||||||
|
author: "Equipe BCards"
|
||||||
|
date: 2025-01-14
|
||||||
|
lastMod: 2025-01-14
|
||||||
|
image: "/images/tutoriais/advogados-bcards.jpg"
|
||||||
|
culture: "pt-BR"
|
||||||
|
category: "advocacia"
|
||||||
|
---
|
||||||
|
|
||||||
|
# BCards para Advogados: Guia Completo de Marketing Digital Ético
|
||||||
|
|
||||||
|
O marketing digital se tornou essencial para advogados que desejam expandir sua base de clientes. No entanto, a advocacia possui regras específicas estabelecidas pela OAB sobre publicidade.
|
||||||
|
|
||||||
|
Neste guia, você aprenderá como usar o BCards de forma profissional, ética e em conformidade com o Código de Ética da OAB.
|
||||||
|
|
||||||
|
## Por Que Advogados Precisam de Presença Digital?
|
||||||
|
|
||||||
|
### Seus Clientes Pesquisam Online
|
||||||
|
|
||||||
|
Quando alguém precisa de um advogado:
|
||||||
|
1. Pesquisa no Google
|
||||||
|
2. Pede indicações em redes sociais
|
||||||
|
3. Verifica perfis e avaliações online
|
||||||
|
|
||||||
|
Se você não está online, está invisível para potenciais clientes.
|
||||||
|
|
||||||
|
### Credibilidade Profissional
|
||||||
|
|
||||||
|
Uma presença digital organizada transmite:
|
||||||
|
- Profissionalismo
|
||||||
|
- Confiabilidade
|
||||||
|
- Acessibilidade
|
||||||
|
- Modernidade
|
||||||
|
|
||||||
|
### Concorrência
|
||||||
|
|
||||||
|
Muitos advogados já estão online. Ficar de fora significa perder espaço no mercado.
|
||||||
|
|
||||||
|
## Regras da OAB Sobre Publicidade Digital
|
||||||
|
|
||||||
|
Antes de criar sua página, é fundamental conhecer as regras do **Provimento n° 205/2021** da OAB:
|
||||||
|
|
||||||
|
### ✅ Permitido:
|
||||||
|
- Informar sobre áreas de atuação
|
||||||
|
- Divulgar títulos e especializações
|
||||||
|
- Publicar conteúdo educativo
|
||||||
|
- Compartilhar experiência profissional
|
||||||
|
- Indicar formas de contato
|
||||||
|
|
||||||
|
### ❌ Proibido:
|
||||||
|
- Garantir resultados
|
||||||
|
- Captação de clientela (mercantilização)
|
||||||
|
- Publicidade agressiva ou sensacionalista
|
||||||
|
- Promessas enganosas
|
||||||
|
- Orçamento sem análise do caso
|
||||||
|
- Comparações depreciativas com colegas
|
||||||
|
|
||||||
|
**Princípio fundamental:** Discrição, sobriedade e informação precisa.
|
||||||
|
|
||||||
|
## Como Configurar Seu BCards Profissional
|
||||||
|
|
||||||
|
### 1. Informações Básicas
|
||||||
|
|
||||||
|
#### Nome da Página
|
||||||
|
Use seu nome profissional completo seguido de "Advogado(a)" ou sua especialização:
|
||||||
|
|
||||||
|
**Exemplos adequados:**
|
||||||
|
- Dra. Maria Silva - Advogada
|
||||||
|
- João Santos | Direito Empresarial
|
||||||
|
- Ana Costa - Advocacia Trabalhista
|
||||||
|
|
||||||
|
**Evite:**
|
||||||
|
- "O Melhor Advogado"
|
||||||
|
- "Ganhe Sua Causa Garantido"
|
||||||
|
- "Advogado Nota 10"
|
||||||
|
|
||||||
|
#### Slug (URL)
|
||||||
|
Crie uma URL profissional:
|
||||||
|
|
||||||
|
**Bons exemplos:**
|
||||||
|
- `bcards.site/page/advocacia/maria-silva-advogada`
|
||||||
|
- `bcards.site/page/advocacia/joao-santos-empresarial`
|
||||||
|
- `bcards.site/page/advocacia/dra-ana-costa`
|
||||||
|
|
||||||
|
#### Descrição
|
||||||
|
Seja claro, objetivo e profissional:
|
||||||
|
|
||||||
|
**Exemplo adequado:**
|
||||||
|
```
|
||||||
|
Advogada especializada em Direito de Família e Sucessões.
|
||||||
|
OAB/SP 123.456 | Mestre em Direito Civil pela USP.
|
||||||
|
Atendimento presencial e online.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Evite:**
|
||||||
|
```
|
||||||
|
A melhor advogada! Ganho 99% dos casos!
|
||||||
|
Atendo urgências a qualquer hora!
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Links Profissionais
|
||||||
|
|
||||||
|
#### Links Essenciais para Advogados
|
||||||
|
|
||||||
|
**1. WhatsApp Business**
|
||||||
|
```
|
||||||
|
Título: Agendar Consulta
|
||||||
|
URL: https://wa.me/5511999999999?text=Olá, gostaria de agendar uma consulta
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Email Profissional**
|
||||||
|
```
|
||||||
|
Título: Contato por Email
|
||||||
|
URL: mailto:contato@seuescritorio.com.br
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Site ou Blog**
|
||||||
|
```
|
||||||
|
Título: Nosso Site
|
||||||
|
URL: https://seuescritorio.com.br
|
||||||
|
```
|
||||||
|
|
||||||
|
**4. LinkedIn**
|
||||||
|
```
|
||||||
|
Título: Perfil Profissional
|
||||||
|
URL: https://linkedin.com/in/seu-perfil
|
||||||
|
```
|
||||||
|
|
||||||
|
**5. Instagram Profissional**
|
||||||
|
```
|
||||||
|
Título: Instagram Jurídico
|
||||||
|
URL: https://instagram.com/seu.escritorio
|
||||||
|
```
|
||||||
|
|
||||||
|
**6. Artigos Jurídicos**
|
||||||
|
```
|
||||||
|
Título: Artigos e Publicações
|
||||||
|
URL: Link para seu blog ou Medium
|
||||||
|
```
|
||||||
|
|
||||||
|
**7. Google Meu Negócio**
|
||||||
|
```
|
||||||
|
Título: Avaliações e Localização
|
||||||
|
URL: Link do Google Maps do escritório
|
||||||
|
```
|
||||||
|
|
||||||
|
**8. Agendamento Online** (se usar)
|
||||||
|
```
|
||||||
|
Título: Agendar Horário
|
||||||
|
URL: Link do Calendly ou sistema de agenda
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Escolha do Tema
|
||||||
|
|
||||||
|
Para advogados, recomendamos temas sóbrios e profissionais:
|
||||||
|
|
||||||
|
- **Clássico**: Fundo branco, clean
|
||||||
|
- **Profissional**: Tons de azul marinho ou cinza
|
||||||
|
- **Minimalista**: Ultra clean e objetivo
|
||||||
|
|
||||||
|
**Evite:**
|
||||||
|
- Cores muito vibrantes
|
||||||
|
- Animações excessivas
|
||||||
|
- Designs infantis ou descontraídos demais
|
||||||
|
|
||||||
|
### 4. Foto de Perfil
|
||||||
|
|
||||||
|
Use uma foto profissional:
|
||||||
|
- Fundo neutro
|
||||||
|
- Vestimenta formal
|
||||||
|
- Boa iluminação
|
||||||
|
- Expressão profissional mas acessível
|
||||||
|
|
||||||
|
**Evite:**
|
||||||
|
- Fotos de festas
|
||||||
|
- Selfies casuais
|
||||||
|
- Fotos em ambientes inadequados
|
||||||
|
|
||||||
|
## Conteúdo para Redes Sociais (Integrando com BCards)
|
||||||
|
|
||||||
|
Seu BCards será o hub central. Nas redes sociais, compartilhe:
|
||||||
|
|
||||||
|
### Conteúdo Educativo Permitido
|
||||||
|
|
||||||
|
**1. Dicas Jurídicas Gerais**
|
||||||
|
```
|
||||||
|
"Você sabia que tem até 2 anos para reclamar de vícios aparentes
|
||||||
|
no imóvel comprado? Entenda seus direitos!"
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Explicações de Leis**
|
||||||
|
```
|
||||||
|
"A Nova Lei de Licitações trouxe mudanças importantes.
|
||||||
|
Resumo das principais alterações: [thread]"
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Mitos e Verdades**
|
||||||
|
```
|
||||||
|
"Mito ou Verdade: É preciso registrar união estável em cartório?
|
||||||
|
MITO. Entenda por quê..."
|
||||||
|
```
|
||||||
|
|
||||||
|
**4. Casos Anônimos (com aprendizado)**
|
||||||
|
```
|
||||||
|
"Caso recente (sem identificação): Cliente conseguiu reaver valor
|
||||||
|
pago indevidamente em plano de saúde. Como? [explica processo genérico]"
|
||||||
|
```
|
||||||
|
|
||||||
|
### O Que NUNCA Postar
|
||||||
|
|
||||||
|
❌ "Acabei de ganhar mais uma causa! Somos imbatíveis!"
|
||||||
|
❌ "100% de sucesso em processos trabalhistas!"
|
||||||
|
❌ "Seu processo pode valer R$ 50.000! Entre em contato!"
|
||||||
|
❌ "Melhor advogado da cidade! Comprovado!"
|
||||||
|
❌ Fotos de audiências ou fóruns sem autorização
|
||||||
|
|
||||||
|
## Estratégia de Marketing Digital Ético
|
||||||
|
|
||||||
|
### 1. Marketing de Conteúdo
|
||||||
|
|
||||||
|
Crie conteúdo valioso:
|
||||||
|
- Artigos em blog
|
||||||
|
- Posts educativos em redes
|
||||||
|
- E-books sobre temas específicos
|
||||||
|
- Vídeos explicativos (YouTube, Instagram)
|
||||||
|
|
||||||
|
**Exemplo de calendário mensal:**
|
||||||
|
- Semana 1: Post sobre mudança legislativa
|
||||||
|
- Semana 2: Dica prática sobre direitos
|
||||||
|
- Semana 3: Explicação de conceito jurídico
|
||||||
|
- Semana 4: Resposta a dúvida comum
|
||||||
|
|
||||||
|
### 2. SEO Local para Advogados
|
||||||
|
|
||||||
|
Otimize para buscas locais:
|
||||||
|
- Google Meu Negócio completo
|
||||||
|
- URL no BCards com sua cidade (se relevante)
|
||||||
|
- Conteúdo mencionando região de atuação
|
||||||
|
|
||||||
|
**Exemplo:**
|
||||||
|
```
|
||||||
|
Advogada especializada em Direito de Família em São Paulo.
|
||||||
|
Atendimento na Zona Sul e Centro.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Peça Avaliações (com Cuidado)
|
||||||
|
|
||||||
|
É permitido ter avaliações no Google Meu Negócio, desde que:
|
||||||
|
- Sejam espontâneas (não solicite diretamente)
|
||||||
|
- Não há oferecimento de benefícios por avaliação
|
||||||
|
- Não são fabricadas
|
||||||
|
|
||||||
|
**Forma adequada:**
|
||||||
|
Após finalizar caso bem-sucedido, você pode:
|
||||||
|
```
|
||||||
|
"Ficamos felizes em ajudá-lo(a). Caso queira, sua opinião
|
||||||
|
sobre nosso atendimento é muito valiosa."
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Networking Digital
|
||||||
|
|
||||||
|
Conecte-se com:
|
||||||
|
- Outros advogados (não concorrentes diretos)
|
||||||
|
- Profissionais complementares (contadores, corretores)
|
||||||
|
- Associações e entidades de classe
|
||||||
|
|
||||||
|
## Estrutura Completa: Exemplo Real
|
||||||
|
|
||||||
|
### Dra. Ana Costa - Direito Trabalhista
|
||||||
|
|
||||||
|
**URL BCards:**
|
||||||
|
`bcards.site/page/advocacia/dra-ana-costa-trabalhista`
|
||||||
|
|
||||||
|
**Descrição:**
|
||||||
|
```
|
||||||
|
Advogada Trabalhista | OAB/SP 234.567
|
||||||
|
Especialista em Direitos do Trabalhador
|
||||||
|
Mestre em Direito do Trabalho pela PUC-SP
|
||||||
|
Atendimento em São Paulo e online
|
||||||
|
```
|
||||||
|
|
||||||
|
**Links na página:**
|
||||||
|
1. 📱 WhatsApp: Agendar Consulta
|
||||||
|
2. 📧 Email: contato@anacosta.adv.br
|
||||||
|
3. 🌐 Site: www.anacosta.adv.br
|
||||||
|
4. 💼 LinkedIn: Perfil Profissional
|
||||||
|
5. 📸 Instagram: @dra.anacosta.trabalhista
|
||||||
|
6. 📄 Blog: Artigos sobre Direito Trabalhista
|
||||||
|
7. 📍 Localização: Escritório no Google Maps
|
||||||
|
8. 🕒 Agendar: Sistema de agendamento online
|
||||||
|
|
||||||
|
**Bio nas redes sociais:**
|
||||||
|
```
|
||||||
|
Dra. Ana Costa | Advogada Trabalhista 👩⚖️
|
||||||
|
OAB/SP 234.567
|
||||||
|
Defendendo direitos dos trabalhadores
|
||||||
|
📍 São Paulo | Atendimento presencial e online
|
||||||
|
🔗 Todos os contatos: [link BCards]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ferramentas Complementares
|
||||||
|
|
||||||
|
### 1. Gestão de Processos
|
||||||
|
- Projuris (jurídico)
|
||||||
|
- Astrea (gestão processual)
|
||||||
|
|
||||||
|
### 2. Agendamento
|
||||||
|
- Calendly
|
||||||
|
- Google Calendar
|
||||||
|
|
||||||
|
### 3. Comunicação
|
||||||
|
- WhatsApp Business
|
||||||
|
- Email profissional (Gmail Workspace)
|
||||||
|
|
||||||
|
### 4. Conteúdo
|
||||||
|
- Canva (design)
|
||||||
|
- Grammarly (revisão de texto)
|
||||||
|
|
||||||
|
### 5. Analytics
|
||||||
|
- Google Analytics (site)
|
||||||
|
- Meta Business Suite (redes sociais)
|
||||||
|
- BCards Analytics (cliques nos links)
|
||||||
|
|
||||||
|
## Monitoramento e Métricas
|
||||||
|
|
||||||
|
Acompanhe mensalmente:
|
||||||
|
|
||||||
|
**No BCards:**
|
||||||
|
- Número de visitantes
|
||||||
|
- Cliques em cada link
|
||||||
|
- Horários de maior acesso
|
||||||
|
|
||||||
|
**Nas Redes Sociais:**
|
||||||
|
- Crescimento de seguidores
|
||||||
|
- Engajamento (curtidas, comentários, compartilhamentos)
|
||||||
|
- Alcance de posts
|
||||||
|
|
||||||
|
**No Negócio:**
|
||||||
|
- Novos clientes vindos do online
|
||||||
|
- Taxa de conversão (visita → consulta)
|
||||||
|
- ROI (retorno sobre investimento em ads)
|
||||||
|
|
||||||
|
## Riscos e Como Evitá-los
|
||||||
|
|
||||||
|
### Risco 1: Infração Ética
|
||||||
|
**Como evitar:**
|
||||||
|
- Revise tudo antes de publicar
|
||||||
|
- Não garanta resultados
|
||||||
|
- Seja discreto e profissional
|
||||||
|
|
||||||
|
### Risco 2: Exposição Inadequada
|
||||||
|
**Como evitar:**
|
||||||
|
- Nunca divulgue detalhes de casos reais identificáveis
|
||||||
|
- Proteja o sigilo profissional sempre
|
||||||
|
- Peça autorização antes de mencionar qualquer caso
|
||||||
|
|
||||||
|
### Risco 3: Comentários Negativos
|
||||||
|
**Como evitar:**
|
||||||
|
- Responda sempre com profissionalismo
|
||||||
|
- Não discuta casos públicos
|
||||||
|
- Se necessário, leve discussão para privado
|
||||||
|
|
||||||
|
### Risco 4: Concorrência Desleal
|
||||||
|
**Como evitar:**
|
||||||
|
- Nunca critique colegas
|
||||||
|
- Foque em seu diferencial, não em defeitos alheios
|
||||||
|
- Colabore, não compita de forma negativa
|
||||||
|
|
||||||
|
## Checklist de Conformidade OAB
|
||||||
|
|
||||||
|
Antes de publicar sua página, verifique:
|
||||||
|
|
||||||
|
- [ ] Informações são verídicas?
|
||||||
|
- [ ] Títulos e especializações estão corretos?
|
||||||
|
- [ ] Não há promessas de resultado?
|
||||||
|
- [ ] Linguagem é sóbria e profissional?
|
||||||
|
- [ ] Não há captação mercantil de clientela?
|
||||||
|
- [ ] Fotos são profissionais?
|
||||||
|
- [ ] Links levam a conteúdo apropriado?
|
||||||
|
- [ ] OAB e número de inscrição estão visíveis?
|
||||||
|
- [ ] Especialização é reconhecida pela OAB (se mencionar)?
|
||||||
|
|
||||||
|
## Dúvidas Frequentes de Advogados
|
||||||
|
|
||||||
|
**P: Posso oferecer primeira consulta gratuita?**
|
||||||
|
R: Sim, desde que seja informação verdadeira e não caracterize captação irregular.
|
||||||
|
|
||||||
|
**P: Posso mencionar clientes famosos que atendi?**
|
||||||
|
R: Não, a menos que tenha autorização expressa e por escrito do cliente.
|
||||||
|
|
||||||
|
**P: Posso colocar preços no BCards?**
|
||||||
|
R: Não é recomendado. Orçamentos devem ser feitos após análise do caso específico.
|
||||||
|
|
||||||
|
**P: Posso fazer anúncios pagos no Google/Instagram?**
|
||||||
|
R: Sim, desde que o conteúdo respeite as normas éticas da OAB.
|
||||||
|
|
||||||
|
**P: Preciso colocar minha OAB no BCards?**
|
||||||
|
R: Sim, é obrigatório identificar número e seccional da OAB.
|
||||||
|
|
||||||
|
## Conclusão
|
||||||
|
|
||||||
|
O BCards é uma ferramenta poderosa para advogados modernizarem sua presença digital mantendo a ética profissional.
|
||||||
|
|
||||||
|
**Principais vantagens para advogados:**
|
||||||
|
- ✅ URL profissional e categorizada
|
||||||
|
- ✅ Centralização de todos os contatos
|
||||||
|
- ✅ Facilita marketing de conteúdo ético
|
||||||
|
- ✅ Transmite credibilidade
|
||||||
|
- ✅ Compatível com normas da OAB
|
||||||
|
- ✅ Analytics para medir resultados
|
||||||
|
|
||||||
|
**Lembre-se:**
|
||||||
|
- Priorize discrição e sobriedade
|
||||||
|
- Foque em educar, não em vender
|
||||||
|
- Seja transparente e honesto
|
||||||
|
- Respeite sempre as normas da OAB
|
||||||
|
|
||||||
|
Sua presença digital pode ser profissional, eficiente e ética ao mesmo tempo.
|
||||||
|
|
||||||
|
**Pronto para criar sua página profissional?**
|
||||||
|
|
||||||
|
[Criar meu BCards para Advocacia →](https://bcards.site/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Referências:**
|
||||||
|
- Provimento OAB n° 205/2021
|
||||||
|
- Código de Ética e Disciplina da OAB
|
||||||
|
|
||||||
|
**Disclaimer:** Este artigo tem caráter informativo. Para dúvidas específicas sobre ética profissional, consulte a OAB de sua seccional.
|
||||||
|
|
||||||
|
**Última atualização:** Janeiro 2025
|
||||||
@ -0,0 +1,359 @@
|
|||||||
|
---
|
||||||
|
title: "Como Criar seu BCard em 5 Minutos: Tutorial Completo"
|
||||||
|
description: "Aprenda passo a passo como criar sua página profissional de links no BCards. Tutorial completo para iniciantes com capturas de tela e dicas práticas."
|
||||||
|
keywords: "tutorial bcards, criar página de links, bio link, tutorial passo a passo"
|
||||||
|
author: "Equipe BCards"
|
||||||
|
date: 2025-01-12
|
||||||
|
lastMod: 2025-01-12
|
||||||
|
image: "/images/tutoriais/criar-bcard.jpg"
|
||||||
|
culture: "pt-BR"
|
||||||
|
category: "tecnologia"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Como Criar seu BCard em 5 Minutos: Tutorial Completo
|
||||||
|
|
||||||
|
Criar sua página profissional de links no BCards é mais fácil do que você imagina. Neste tutorial, vou te guiar passo a passo desde o cadastro até a publicação da sua página.
|
||||||
|
|
||||||
|
## O Que Você Vai Precisar
|
||||||
|
|
||||||
|
- Email válido
|
||||||
|
- Conta no Google ou Microsoft (para login rápido)
|
||||||
|
- 5 minutos do seu tempo
|
||||||
|
- Links que você quer compartilhar
|
||||||
|
|
||||||
|
## Passo 1: Criar Sua Conta
|
||||||
|
|
||||||
|
### 1.1. Acesse o Site
|
||||||
|
|
||||||
|
Vá para [bcards.site](https://bcards.site) e clique em **"Entrar"** no menu superior.
|
||||||
|
|
||||||
|
### 1.2. Escolha o Método de Login
|
||||||
|
|
||||||
|
Você tem duas opções:
|
||||||
|
|
||||||
|
**Opção A: Login com Google**
|
||||||
|
- Clique em "Entrar com Google"
|
||||||
|
- Selecione sua conta
|
||||||
|
- Autorize o acesso
|
||||||
|
|
||||||
|
**Opção B: Login com Microsoft**
|
||||||
|
- Clique em "Entrar com Microsoft"
|
||||||
|
- Insira suas credenciais
|
||||||
|
- Autorize o acesso
|
||||||
|
|
||||||
|
✅ **Dica:** Usar login social é mais rápido e seguro (sem necessidade de criar nova senha).
|
||||||
|
|
||||||
|
## Passo 2: Acessar o Dashboard
|
||||||
|
|
||||||
|
Após fazer login, você será redirecionado para seu **Dashboard**. Este é o painel de controle onde você gerencia sua página.
|
||||||
|
|
||||||
|
No Dashboard você verá:
|
||||||
|
- Botão "Criar Minha Página" (se é sua primeira vez)
|
||||||
|
- Estatísticas (após criar a página)
|
||||||
|
- Links para editar sua página
|
||||||
|
|
||||||
|
## Passo 3: Criar Sua Página
|
||||||
|
|
||||||
|
### 3.1. Clique em "Criar Minha Página"
|
||||||
|
|
||||||
|
Você verá um formulário com vários campos. Vamos preencher cada um:
|
||||||
|
|
||||||
|
### 3.2. Informações Básicas
|
||||||
|
|
||||||
|
**Nome da Página:**
|
||||||
|
- Digite como você quer ser identificado
|
||||||
|
- Exemplo: "João Silva", "Maria Design", "Advocacia Santos"
|
||||||
|
- Este será o título principal da sua página
|
||||||
|
|
||||||
|
**Categoria:**
|
||||||
|
- Selecione a categoria que melhor representa sua atividade
|
||||||
|
- Exemplos disponíveis:
|
||||||
|
- Tecnologia
|
||||||
|
- Advocacia
|
||||||
|
- Saúde
|
||||||
|
- Educação
|
||||||
|
- Marketing
|
||||||
|
- E muitas outras...
|
||||||
|
|
||||||
|
✅ **Dica:** A categoria fará parte da sua URL e ajuda no SEO.
|
||||||
|
|
||||||
|
**Slug (URL Personalizada):**
|
||||||
|
- Este será o final da sua URL
|
||||||
|
- Formato final: `bcards.site/page/{categoria}/{seu-slug}`
|
||||||
|
- Use apenas letras minúsculas, números e hifens
|
||||||
|
- Sem espaços, acentos ou caracteres especiais
|
||||||
|
|
||||||
|
**Exemplos:**
|
||||||
|
- `joao-silva-dev` → bcards.site/page/tecnologia/joao-silva-dev
|
||||||
|
- `maria-designer` → bcards.site/page/design/maria-designer
|
||||||
|
- `dra-ana-pediatra` → bcards.site/page/saude/dra-ana-pediatra
|
||||||
|
|
||||||
|
### 3.3. Descrição
|
||||||
|
|
||||||
|
Digite uma breve descrição sobre você ou seu negócio:
|
||||||
|
|
||||||
|
**Bom exemplo:**
|
||||||
|
```
|
||||||
|
Desenvolvedora Full Stack especializada em React e Node.js.
|
||||||
|
Ajudo empresas a criarem aplicações web modernas e escaláveis.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Evite:**
|
||||||
|
```
|
||||||
|
Desenvolvedora
|
||||||
|
```
|
||||||
|
(muito curto, sem contexto)
|
||||||
|
|
||||||
|
✅ **Dica:** Use 2-3 frases. Seja claro e direto.
|
||||||
|
|
||||||
|
### 3.4. Escolha do Tema Visual
|
||||||
|
|
||||||
|
Role até a seção "Tema" e selecione o visual da sua página:
|
||||||
|
|
||||||
|
**Temas disponíveis (variam por plano):**
|
||||||
|
- **Clássico**: Fundo branco limpo, ideal para profissionais
|
||||||
|
- **Dark**: Fundo escuro, moderno
|
||||||
|
- **Gradiente**: Cores vibrantes em degradê
|
||||||
|
- **Minimalista**: Ultra clean
|
||||||
|
- **Profissional**: Sóbrio e corporativo
|
||||||
|
|
||||||
|
Você pode trocar o tema depois a qualquer momento.
|
||||||
|
|
||||||
|
### 3.5. Adicionar Links
|
||||||
|
|
||||||
|
Agora vamos adicionar os links que aparecerão na sua página.
|
||||||
|
|
||||||
|
**Para adicionar um link:**
|
||||||
|
|
||||||
|
1. Clique em **"Adicionar Link"**
|
||||||
|
2. Preencha os campos:
|
||||||
|
- **Título**: Nome que aparecerá no botão
|
||||||
|
- **URL**: Endereço completo (começando com https://)
|
||||||
|
- **Ícone** (opcional): Escolha um ícone para o link
|
||||||
|
|
||||||
|
**Exemplo de Link:**
|
||||||
|
```
|
||||||
|
Título: Meu Portfólio
|
||||||
|
URL: https://meusite.com.br
|
||||||
|
Ícone: fa-briefcase
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tipos de Links Comuns:**
|
||||||
|
|
||||||
|
- WhatsApp: `https://wa.me/5511999999999`
|
||||||
|
- Instagram: `https://instagram.com/seuusuario`
|
||||||
|
- Facebook: `https://facebook.com/suapagina`
|
||||||
|
- LinkedIn: `https://linkedin.com/in/seuusuario`
|
||||||
|
- YouTube: `https://youtube.com/@seucanal`
|
||||||
|
- Site: `https://seusite.com.br`
|
||||||
|
- Email: `mailto:seu@email.com`
|
||||||
|
|
||||||
|
✅ **Dica:** Organize links por importância. Os primeiros aparecem no topo da página.
|
||||||
|
|
||||||
|
### 3.6. Reordenar Links
|
||||||
|
|
||||||
|
Você pode arrastar e soltar os links para mudar a ordem de exibição. Os links mais importantes devem ficar no topo.
|
||||||
|
|
||||||
|
## Passo 4: Personalização Avançada (Opcional)
|
||||||
|
|
||||||
|
### 4.1. Foto de Perfil
|
||||||
|
|
||||||
|
Upload de uma foto profissional (recomendado):
|
||||||
|
- Tamanho ideal: 400x400 pixels
|
||||||
|
- Formato: JPG ou PNG
|
||||||
|
- Peso: Até 2MB
|
||||||
|
|
||||||
|
### 4.2. Cores Personalizadas (Plano Premium)
|
||||||
|
|
||||||
|
Se você tem plano Premium, pode personalizar:
|
||||||
|
- Cor dos botões
|
||||||
|
- Cor do fundo
|
||||||
|
- Cor do texto
|
||||||
|
- Fontes
|
||||||
|
|
||||||
|
### 4.3. Redes Sociais
|
||||||
|
|
||||||
|
Adicione seus perfis sociais na seção específica. Eles aparecerão como ícones na sua página.
|
||||||
|
|
||||||
|
## Passo 5: Salvar e Submeter para Moderação
|
||||||
|
|
||||||
|
### 5.1. Revisar Tudo
|
||||||
|
|
||||||
|
Antes de salvar, revise:
|
||||||
|
- ✅ Nome está correto?
|
||||||
|
- ✅ URL (slug) está como você quer?
|
||||||
|
- ✅ Todos os links funcionam?
|
||||||
|
- ✅ Descrição está clara?
|
||||||
|
- ✅ Tema escolhido te agrada?
|
||||||
|
|
||||||
|
### 5.2. Salvar
|
||||||
|
|
||||||
|
Clique no botão **"Salvar Página"** no final do formulário.
|
||||||
|
|
||||||
|
### 5.3. Submeter para Moderação
|
||||||
|
|
||||||
|
Após salvar, você verá sua página em modo de **preview** (visualização).
|
||||||
|
|
||||||
|
Para torná-la pública, clique em **"Submeter para Moderação"**.
|
||||||
|
|
||||||
|
**O Que Acontece Agora?**
|
||||||
|
1. Nossa equipe revisa sua página (24-48 horas)
|
||||||
|
2. Verificamos se segue as diretrizes da comunidade
|
||||||
|
3. Você recebe email quando for aprovada
|
||||||
|
4. Sua página fica pública!
|
||||||
|
|
||||||
|
## Passo 6: Visualizar e Compartilhar
|
||||||
|
|
||||||
|
### 6.1. Token de Preview
|
||||||
|
|
||||||
|
Enquanto aguarda aprovação, você pode visualizar sua página usando o **token de preview**:
|
||||||
|
|
||||||
|
1. No Dashboard, clique em "Gerar Token de Preview"
|
||||||
|
2. Copie o link gerado
|
||||||
|
3. Abra em uma aba privada ou compartilhe com amigos
|
||||||
|
|
||||||
|
**Exemplo de link de preview:**
|
||||||
|
```
|
||||||
|
bcards.site/page/tecnologia/joao-dev?preview=ABC123XYZ
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2. Após Aprovação
|
||||||
|
|
||||||
|
Quando sua página for aprovada:
|
||||||
|
- Ela estará acessível publicamente
|
||||||
|
- URL final: `bcards.site/page/{categoria}/{seu-slug}`
|
||||||
|
- Você pode compartilhar em suas redes sociais!
|
||||||
|
|
||||||
|
## Passo 7: Atualizar Bio das Redes Sociais
|
||||||
|
|
||||||
|
Com sua página aprovada, atualize a bio de todas as suas redes:
|
||||||
|
|
||||||
|
### Instagram
|
||||||
|
1. Vá em "Editar Perfil"
|
||||||
|
2. Cole seu link BCards no campo "Website"
|
||||||
|
3. Salve
|
||||||
|
|
||||||
|
### TikTok
|
||||||
|
1. Editar Perfil
|
||||||
|
2. Cole o link em "Bio"
|
||||||
|
3. Salve
|
||||||
|
|
||||||
|
### Twitter/X
|
||||||
|
1. Editar Perfil
|
||||||
|
2. Cole o link em "Website"
|
||||||
|
3. Salve
|
||||||
|
|
||||||
|
### LinkedIn
|
||||||
|
1. Editar Perfil
|
||||||
|
2. Cole na seção "Informações de Contato"
|
||||||
|
3. Salve
|
||||||
|
|
||||||
|
## Dicas de Boas Práticas
|
||||||
|
|
||||||
|
### ✅ Faça:
|
||||||
|
- Use foto profissional nítida
|
||||||
|
- Escreva descrição clara e objetiva
|
||||||
|
- Teste todos os links antes de publicar
|
||||||
|
- Mantenha a página atualizada
|
||||||
|
- Responda mensagens rapidamente
|
||||||
|
- Use call-to-action nos títulos dos links
|
||||||
|
|
||||||
|
### ❌ Evite:
|
||||||
|
- Links quebrados ou incorretos
|
||||||
|
- Descrição muito longa ou muito curta
|
||||||
|
- Excesso de links (foco no essencial)
|
||||||
|
- Informações enganosas
|
||||||
|
- Conteúdo que viola diretrizes
|
||||||
|
|
||||||
|
## Atualizando Sua Página Depois
|
||||||
|
|
||||||
|
Você pode editar sua página a qualquer momento:
|
||||||
|
|
||||||
|
1. Acesse o Dashboard
|
||||||
|
2. Clique em "Editar Página"
|
||||||
|
3. Faça as alterações
|
||||||
|
4. Salve
|
||||||
|
|
||||||
|
**Importante:** Mudanças significativas podem exigir nova moderação.
|
||||||
|
|
||||||
|
## Planos e Upgrades
|
||||||
|
|
||||||
|
### Plano Básico (R$ 12,90/mês)
|
||||||
|
- 5 links
|
||||||
|
- Temas básicos
|
||||||
|
- Analytics essenciais
|
||||||
|
|
||||||
|
### Plano Profissional (R$ 25,90/mês)
|
||||||
|
- 15 links
|
||||||
|
- Todos os temas
|
||||||
|
- Analytics completo
|
||||||
|
- Suporte prioritário
|
||||||
|
|
||||||
|
### Plano Premium (R$ 29,90/mês)
|
||||||
|
- Links ilimitados
|
||||||
|
- Customização total
|
||||||
|
- Temas exclusivos
|
||||||
|
- Editor CSS
|
||||||
|
- Suporte VIP
|
||||||
|
|
||||||
|
Para fazer upgrade:
|
||||||
|
1. Dashboard → "Meu Plano"
|
||||||
|
2. Escolha o plano desejado
|
||||||
|
3. Preencha dados de pagamento
|
||||||
|
4. Confirme
|
||||||
|
|
||||||
|
## Analisando Resultados
|
||||||
|
|
||||||
|
Após sua página estar ativa, acompanhe as métricas no Dashboard:
|
||||||
|
|
||||||
|
- **Total de visitantes**: Quantas pessoas acessaram
|
||||||
|
- **Cliques por link**: Quais links são mais populares
|
||||||
|
- **Origem do tráfico**: De onde vieram os visitantes
|
||||||
|
|
||||||
|
Use esses dados para otimizar sua página!
|
||||||
|
|
||||||
|
## Problemas Comuns e Soluções
|
||||||
|
|
||||||
|
### "Meu slug já está em uso"
|
||||||
|
**Solução:** Escolha outro slug. Tente adicionar seu nome, cidade ou especialidade.
|
||||||
|
|
||||||
|
### "Link não funciona"
|
||||||
|
**Solução:** Verifique se a URL começa com `https://` e está digitada corretamente.
|
||||||
|
|
||||||
|
### "Página foi rejeitada na moderação"
|
||||||
|
**Solução:** Leia o email com o motivo da rejeição, ajuste conforme orientações e resubmeta.
|
||||||
|
|
||||||
|
### "Não recebi email de aprovação"
|
||||||
|
**Solução:** Verifique spam/lixo eletrônico ou entre em contato com suporte.
|
||||||
|
|
||||||
|
## Suporte
|
||||||
|
|
||||||
|
Precisa de ajuda?
|
||||||
|
|
||||||
|
- **Email**: suporte@bcards.site
|
||||||
|
- **Horário**: Segunda a sexta, 9h às 18h
|
||||||
|
- **FAQ**: bcards.site/ajuda
|
||||||
|
- **WhatsApp**: (Link no site)
|
||||||
|
|
||||||
|
## Conclusão
|
||||||
|
|
||||||
|
Parabéns! 🎉 Agora você sabe criar sua página profissional no BCards do zero.
|
||||||
|
|
||||||
|
**Recapitulando:**
|
||||||
|
1. ✅ Criar conta
|
||||||
|
2. ✅ Preencher informações
|
||||||
|
3. ✅ Escolher tema
|
||||||
|
4. ✅ Adicionar links
|
||||||
|
5. ✅ Salvar e submeter
|
||||||
|
6. ✅ Aguardar aprovação
|
||||||
|
7. ✅ Compartilhar!
|
||||||
|
|
||||||
|
Sua presença online profissional está a apenas alguns cliques de distância.
|
||||||
|
|
||||||
|
**Pronto para começar?** [Criar meu BCard agora →](https://bcards.site/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Tempo de leitura:** 8 minutos
|
||||||
|
**Dificuldade:** Iniciante
|
||||||
|
**Última atualização:** Janeiro 2025
|
||||||
@ -5,9 +5,13 @@ using BCards.Web.ViewModels;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Linq;
|
||||||
|
using MongoDB.Bson;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace BCards.Web.Controllers;
|
namespace BCards.Web.Controllers;
|
||||||
|
|
||||||
@ -23,9 +27,11 @@ 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 IDocumentStorageService _documentStorage;
|
||||||
private readonly IPaymentService _paymentService;
|
private readonly IPaymentService _paymentService;
|
||||||
private readonly IDowngradeService _downgradeService;
|
private readonly IDowngradeService _downgradeService;
|
||||||
private readonly ILogger<AdminController> _logger;
|
private readonly ILogger<AdminController> _logger;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
public AdminController(
|
public AdminController(
|
||||||
IAuthService authService,
|
IAuthService authService,
|
||||||
@ -36,9 +42,11 @@ public class AdminController : Controller
|
|||||||
IEmailService emailService,
|
IEmailService emailService,
|
||||||
ILivePageService livePageService,
|
ILivePageService livePageService,
|
||||||
IImageStorageService imageStorage,
|
IImageStorageService imageStorage,
|
||||||
|
IDocumentStorageService documentStorage,
|
||||||
IPaymentService paymentService,
|
IPaymentService paymentService,
|
||||||
IDowngradeService downgradeService,
|
IDowngradeService downgradeService,
|
||||||
ILogger<AdminController> logger)
|
ILogger<AdminController> logger,
|
||||||
|
IConfiguration configuration)
|
||||||
{
|
{
|
||||||
_authService = authService;
|
_authService = authService;
|
||||||
_userPageService = userPageService;
|
_userPageService = userPageService;
|
||||||
@ -48,9 +56,11 @@ public class AdminController : Controller
|
|||||||
_emailService = emailService;
|
_emailService = emailService;
|
||||||
_livePageService = livePageService;
|
_livePageService = livePageService;
|
||||||
_imageStorage = imageStorage;
|
_imageStorage = imageStorage;
|
||||||
|
_documentStorage = documentStorage;
|
||||||
_paymentService = paymentService;
|
_paymentService = paymentService;
|
||||||
_downgradeService = downgradeService;
|
_downgradeService = downgradeService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_configuration = configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -175,7 +185,11 @@ public class AdminController : Controller
|
|||||||
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
|
AllowProductLinks = planLimitations.AllowProductLinks,
|
||||||
|
AllowDocumentUpload = planLimitations.AllowDocumentUpload,
|
||||||
|
MaxDocumentsAllowed = planLimitations.MaxDocuments,
|
||||||
|
Documents = new List<ManageDocumentViewModel>(),
|
||||||
|
DocumentUploadPlansDisplay = GetDocumentUploadPlansDisplay()
|
||||||
};
|
};
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
@ -214,6 +228,12 @@ public class AdminController : Controller
|
|||||||
return RedirectToAction("Login", "Auth");
|
return RedirectToAction("Login", "Auth");
|
||||||
|
|
||||||
userId = user.Id;
|
userId = user.Id;
|
||||||
|
var userPlanType = Enum.TryParse<PlanType>(user.CurrentPlan, true, out var parsedPlan) ? parsedPlan : PlanType.Trial;
|
||||||
|
var planLimitations = await _paymentService.GetPlanLimitationsAsync(userPlanType.ToString());
|
||||||
|
model.AllowProductLinks = planLimitations.AllowProductLinks;
|
||||||
|
model.AllowDocumentUpload = planLimitations.AllowDocumentUpload;
|
||||||
|
model.MaxDocumentsAllowed = planLimitations.MaxDocuments;
|
||||||
|
model.DocumentUploadPlansDisplay = GetDocumentUploadPlansDisplay();
|
||||||
|
|
||||||
// Limpar campos de redes sociais que são apenas espaços (tratados como vazios)
|
// Limpar campos de redes sociais que são apenas espaços (tratados como vazios)
|
||||||
CleanSocialMediaFields(model);
|
CleanSocialMediaFields(model);
|
||||||
@ -255,13 +275,13 @@ public class AdminController : Controller
|
|||||||
TempData["ImageError"] = errorMessage;
|
TempData["ImageError"] = errorMessage;
|
||||||
|
|
||||||
// Preservar dados do form e repopular dropdowns
|
// Preservar dados do form e repopular dropdowns
|
||||||
var userPlanType = Enum.TryParse<PlanType>(user.CurrentPlan, true, out var planType) ? planType : PlanType.Trial;
|
|
||||||
var planLimitations = await _paymentService.GetPlanLimitationsAsync(userPlanType.ToString());
|
|
||||||
|
|
||||||
model.AvailableCategories = await _categoryService.GetAllCategoriesAsync();
|
model.AvailableCategories = await _categoryService.GetAllCategoriesAsync();
|
||||||
model.AvailableThemes = await _themeService.GetAvailableThemesAsync();
|
model.AvailableThemes = await _themeService.GetAvailableThemesAsync();
|
||||||
model.MaxLinksAllowed = userPlanType.GetMaxLinksPerPage();
|
model.MaxLinksAllowed = userPlanType.GetMaxLinksPerPage();
|
||||||
model.AllowProductLinks = planLimitations.AllowProductLinks;
|
model.AllowProductLinks = planLimitations.AllowProductLinks;
|
||||||
|
model.AllowDocumentUpload = planLimitations.AllowDocumentUpload;
|
||||||
|
model.MaxDocumentsAllowed = planLimitations.MaxDocuments;
|
||||||
|
model.DocumentUploadPlansDisplay = GetDocumentUploadPlansDisplay();
|
||||||
|
|
||||||
// Preservar ProfileImageId existente se estava editando
|
// Preservar ProfileImageId existente se estava editando
|
||||||
if (!model.IsNewPage && !string.IsNullOrEmpty(model.Id))
|
if (!model.IsNewPage && !string.IsNullOrEmpty(model.Id))
|
||||||
@ -277,6 +297,8 @@ public class AdminController : Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (processedDocuments, removedFileIds, newFileIds) = await BuildDocumentsAsync(model, planLimitations);
|
||||||
|
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
{
|
{
|
||||||
var sbError = new StringBuilder();
|
var sbError = new StringBuilder();
|
||||||
@ -297,6 +319,10 @@ public class AdminController : Controller
|
|||||||
model.Slug = slug;
|
model.Slug = slug;
|
||||||
model.AvailableCategories = await _categoryService.GetAllCategoriesAsync();
|
model.AvailableCategories = await _categoryService.GetAllCategoriesAsync();
|
||||||
model.AvailableThemes = await _themeService.GetAvailableThemesAsync();
|
model.AvailableThemes = await _themeService.GetAvailableThemesAsync();
|
||||||
|
model.AllowProductLinks = planLimitations.AllowProductLinks;
|
||||||
|
model.AllowDocumentUpload = planLimitations.AllowDocumentUpload;
|
||||||
|
model.MaxDocumentsAllowed = planLimitations.MaxDocuments;
|
||||||
|
model.DocumentUploadPlansDisplay = GetDocumentUploadPlansDisplay();
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -304,7 +330,6 @@ public class AdminController : Controller
|
|||||||
{
|
{
|
||||||
// CRITICAL: Check if user can create new page (validate MaxPages limit)
|
// CRITICAL: Check if user can create new page (validate MaxPages limit)
|
||||||
var existingPages = await _userPageService.GetUserPagesAsync(user.Id);
|
var existingPages = await _userPageService.GetUserPagesAsync(user.Id);
|
||||||
var userPlanType = Enum.TryParse<PlanType>(user.CurrentPlan, true, out var planType) ? planType : PlanType.Trial;
|
|
||||||
var maxPages = userPlanType.GetMaxPages();
|
var maxPages = userPlanType.GetMaxPages();
|
||||||
|
|
||||||
if (existingPages.Count >= maxPages)
|
if (existingPages.Count >= maxPages)
|
||||||
@ -312,6 +337,7 @@ public class AdminController : Controller
|
|||||||
TempData["Error"] = $"Você já atingiu o limite de {maxPages} página(s) do seu plano atual. Faça upgrade para criar mais páginas.";
|
TempData["Error"] = $"Você já atingiu o limite de {maxPages} página(s) do seu plano atual. Faça upgrade para criar mais páginas.";
|
||||||
model.AvailableCategories = await _categoryService.GetAllCategoriesAsync();
|
model.AvailableCategories = await _categoryService.GetAllCategoriesAsync();
|
||||||
model.AvailableThemes = await _themeService.GetAvailableThemesAsync();
|
model.AvailableThemes = await _themeService.GetAvailableThemesAsync();
|
||||||
|
model.DocumentUploadPlansDisplay = GetDocumentUploadPlansDisplay();
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -327,6 +353,7 @@ public class AdminController : Controller
|
|||||||
ModelState.AddModelError("Slug", "Esta URL já está em uso. Tente outra.");
|
ModelState.AddModelError("Slug", "Esta URL já está em uso. Tente outra.");
|
||||||
model.AvailableCategories = await _categoryService.GetAllCategoriesAsync();
|
model.AvailableCategories = await _categoryService.GetAllCategoriesAsync();
|
||||||
model.AvailableThemes = await _themeService.GetAvailableThemesAsync();
|
model.AvailableThemes = await _themeService.GetAvailableThemesAsync();
|
||||||
|
model.DocumentUploadPlansDisplay = GetDocumentUploadPlansDisplay();
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -337,6 +364,7 @@ public class AdminController : Controller
|
|||||||
ModelState.AddModelError("", $"Você excedeu o limite de {model.MaxLinksAllowed} links do seu plano atual.");
|
ModelState.AddModelError("", $"Você excedeu o limite de {model.MaxLinksAllowed} links do seu plano atual.");
|
||||||
model.AvailableCategories = await _categoryService.GetAllCategoriesAsync();
|
model.AvailableCategories = await _categoryService.GetAllCategoriesAsync();
|
||||||
model.AvailableThemes = await _themeService.GetAvailableThemesAsync();
|
model.AvailableThemes = await _themeService.GetAvailableThemesAsync();
|
||||||
|
model.DocumentUploadPlansDisplay = GetDocumentUploadPlansDisplay();
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,6 +372,7 @@ public class AdminController : Controller
|
|||||||
{
|
{
|
||||||
// Create new page
|
// Create new page
|
||||||
var userPage = await MapToUserPage(model, user.Id);
|
var userPage = await MapToUserPage(model, user.Id);
|
||||||
|
userPage.Documents = processedDocuments;
|
||||||
_logger.LogInformation($"Mapped to UserPage: {userPage.DisplayName}, Category: {userPage.Category}, Slug: {userPage.Slug}");
|
_logger.LogInformation($"Mapped to UserPage: {userPage.DisplayName}, Category: {userPage.Category}, Slug: {userPage.Slug}");
|
||||||
|
|
||||||
// Set status to Creating for new pages
|
// Set status to Creating for new pages
|
||||||
@ -362,6 +391,7 @@ public class AdminController : Controller
|
|||||||
ModelState.AddModelError("", "Erro ao criar página. Tente novamente.");
|
ModelState.AddModelError("", "Erro ao criar página. Tente novamente.");
|
||||||
model.AvailableCategories = await _categoryService.GetAllCategoriesAsync();
|
model.AvailableCategories = await _categoryService.GetAllCategoriesAsync();
|
||||||
model.AvailableThemes = await _themeService.GetAvailableThemesAsync();
|
model.AvailableThemes = await _themeService.GetAvailableThemesAsync();
|
||||||
|
model.DocumentUploadPlansDisplay = GetDocumentUploadPlansDisplay();
|
||||||
TempData["Error"] = $"Erro ao criar página. Tente novamente. TechMsg: {ex.Message}";
|
TempData["Error"] = $"Erro ao criar página. Tente novamente. TechMsg: {ex.Message}";
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
@ -371,7 +401,15 @@ public class AdminController : Controller
|
|||||||
// Update existing page
|
// Update existing page
|
||||||
var existingPage = await _userPageService.GetPageByIdAsync(model.Id);
|
var existingPage = await _userPageService.GetPageByIdAsync(model.Id);
|
||||||
if (existingPage == null || existingPage.UserId != user.Id)
|
if (existingPage == null || existingPage.UserId != user.Id)
|
||||||
|
{
|
||||||
|
await CleanupNewDocumentsAsync(newFileIds);
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!planLimitations.AllowDocumentUpload && existingPage.Documents?.Any() == true)
|
||||||
|
{
|
||||||
|
removedFileIds.AddRange(existingPage.Documents.Select(d => d.FileId));
|
||||||
|
}
|
||||||
|
|
||||||
// Check if user can create pages (for users with rejected pages)
|
// Check if user can create pages (for users with rejected pages)
|
||||||
var canCreatePage = await _moderationService.CanUserCreatePageAsync(user.Id);
|
var canCreatePage = await _moderationService.CanUserCreatePageAsync(user.Id);
|
||||||
@ -397,7 +435,7 @@ public class AdminController : Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await UpdateUserPageFromModel(existingPage, model);
|
await UpdateUserPageFromModel(existingPage, model, processedDocuments);
|
||||||
|
|
||||||
// Set status to PendingModeration for updates
|
// Set status to PendingModeration for updates
|
||||||
existingPage.Status = ViewModels.PageStatus.Creating;
|
existingPage.Status = ViewModels.PageStatus.Creating;
|
||||||
@ -405,6 +443,21 @@ public class AdminController : Controller
|
|||||||
|
|
||||||
await _userPageService.UpdatePageAsync(existingPage);
|
await _userPageService.UpdatePageAsync(existingPage);
|
||||||
|
|
||||||
|
if (removedFileIds.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var fileId in removedFileIds)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _documentStorage.DeleteDocumentAsync(fileId);
|
||||||
|
}
|
||||||
|
catch (Exception cleanupEx)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(cleanupEx, "Erro ao remover documento antigo {FileId}", fileId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Token será gerado apenas quando usuário clicar "Testar Página"
|
// Token será gerado apenas quando usuário clicar "Testar Página"
|
||||||
|
|
||||||
// Send email to user
|
// Send email to user
|
||||||
@ -745,6 +798,8 @@ public class AdminController : Controller
|
|||||||
|
|
||||||
private async Task<ManagePageViewModel> MapToManageViewModel(UserPage page, List<Category> categories, List<PageTheme> themes, PlanType userPlanType)
|
private async Task<ManagePageViewModel> MapToManageViewModel(UserPage page, List<Category> categories, List<PageTheme> themes, PlanType userPlanType)
|
||||||
{
|
{
|
||||||
|
var planLimitations = await _paymentService.GetPlanLimitationsAsync(userPlanType.ToString());
|
||||||
|
|
||||||
return new ManagePageViewModel
|
return new ManagePageViewModel
|
||||||
{
|
{
|
||||||
Id = page.Id,
|
Id = page.Id,
|
||||||
@ -772,10 +827,23 @@ public class AdminController : Controller
|
|||||||
ProductDescription = l.ProductDescription,
|
ProductDescription = l.ProductDescription,
|
||||||
ProductDataCachedAt = l.ProductDataCachedAt
|
ProductDataCachedAt = l.ProductDataCachedAt
|
||||||
}).ToList() ?? new List<ManageLinkViewModel>(),
|
}).ToList() ?? new List<ManageLinkViewModel>(),
|
||||||
|
Documents = page.Documents?.Select((d, index) => new ManageDocumentViewModel
|
||||||
|
{
|
||||||
|
Id = string.IsNullOrEmpty(d.Id) ? $"doc_{index}" : d.Id,
|
||||||
|
DocumentId = d.FileId,
|
||||||
|
Title = d.Title,
|
||||||
|
Description = d.Description,
|
||||||
|
FileName = d.FileName,
|
||||||
|
FileSize = d.FileSize,
|
||||||
|
UploadedAt = d.UploadedAt
|
||||||
|
}).ToList() ?? new List<ManageDocumentViewModel>(),
|
||||||
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
|
AllowProductLinks = planLimitations.AllowProductLinks,
|
||||||
|
AllowDocumentUpload = planLimitations.AllowDocumentUpload,
|
||||||
|
MaxDocumentsAllowed = planLimitations.MaxDocuments,
|
||||||
|
DocumentUploadPlansDisplay = GetDocumentUploadPlansDisplay()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -922,7 +990,198 @@ public class AdminController : Controller
|
|||||||
return userPage;
|
return userPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UpdateUserPageFromModel(UserPage page, ManagePageViewModel model)
|
private string GetDocumentUploadPlansDisplay()
|
||||||
|
{
|
||||||
|
var sections = _configuration.GetSection("Plans").GetChildren();
|
||||||
|
|
||||||
|
var friendlyNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var section in sections)
|
||||||
|
{
|
||||||
|
if (!bool.TryParse(section["AllowDocumentUpload"], out var allow) || !allow)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var key = section.Key ?? string.Empty;
|
||||||
|
var normalizedKey = key.EndsWith("Yearly", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? key[..^6]
|
||||||
|
: key;
|
||||||
|
|
||||||
|
if (Enum.TryParse<PlanType>(normalizedKey, true, out var planType))
|
||||||
|
{
|
||||||
|
friendlyNames.Add(planType.GetDisplayName());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var display = section["Name"] ?? key;
|
||||||
|
if (!string.IsNullOrWhiteSpace(display))
|
||||||
|
{
|
||||||
|
friendlyNames.Add(display);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (friendlyNames.Count == 0)
|
||||||
|
return "planos com suporte a documentos";
|
||||||
|
|
||||||
|
var orderedNames = friendlyNames.OrderBy(name => name).ToList();
|
||||||
|
|
||||||
|
return orderedNames.Count switch
|
||||||
|
{
|
||||||
|
1 => orderedNames[0],
|
||||||
|
2 => string.Join(" e ", orderedNames),
|
||||||
|
_ => $"{string.Join(", ", orderedNames.Take(orderedNames.Count - 1))} e {orderedNames.Last()}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(List<PageDocument> Documents, List<string> RemovedFileIds, List<string> NewlyUploadedFileIds)> BuildDocumentsAsync(ManagePageViewModel model, PlanLimitations planLimitations)
|
||||||
|
{
|
||||||
|
var documents = new List<PageDocument>();
|
||||||
|
var removedFileIds = new List<string>();
|
||||||
|
var newFileIds = new List<string>();
|
||||||
|
|
||||||
|
if (!planLimitations.AllowDocumentUpload)
|
||||||
|
{
|
||||||
|
return (documents, removedFileIds, newFileIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.Documents == null || model.Documents.Count == 0)
|
||||||
|
return (documents, removedFileIds, newFileIds);
|
||||||
|
|
||||||
|
for (int index = 0; index < model.Documents.Count; index++)
|
||||||
|
{
|
||||||
|
var docVm = model.Documents[index];
|
||||||
|
if (docVm == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var hasExisting = !string.IsNullOrEmpty(docVm.DocumentId);
|
||||||
|
|
||||||
|
if (docVm.MarkForRemoval)
|
||||||
|
{
|
||||||
|
if (hasExisting && docVm.DocumentId != null)
|
||||||
|
removedFileIds.Add(docVm.DocumentId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(docVm.Title))
|
||||||
|
{
|
||||||
|
ModelState.AddModelError($"Documents[{index}].Title", "Título é obrigatório");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string fileId = docVm.DocumentId?.Trim() ?? string.Empty;
|
||||||
|
string fileName = docVm.FileName?.Trim() ?? string.Empty;
|
||||||
|
long fileSize = docVm.FileSize;
|
||||||
|
var uploadedAt = docVm.UploadedAt ?? DateTime.UtcNow;
|
||||||
|
|
||||||
|
if (hasExisting && string.IsNullOrEmpty(docVm.Id))
|
||||||
|
{
|
||||||
|
docVm.Id = ObjectId.GenerateNewId().ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (docVm.DocumentFile != null && docVm.DocumentFile.Length > 0)
|
||||||
|
{
|
||||||
|
if (!docVm.DocumentFile.ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
ModelState.AddModelError($"Documents[{index}].DocumentFile", "Envie um arquivo em PDF.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (docVm.DocumentFile.Length > 10 * 1024 * 1024)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError($"Documents[{index}].DocumentFile", "Arquivo muito grande. Tamanho máximo: 10MB.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var memoryStream = new MemoryStream();
|
||||||
|
await docVm.DocumentFile.CopyToAsync(memoryStream);
|
||||||
|
var documentBytes = memoryStream.ToArray();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fileId = await _documentStorage.SaveDocumentAsync(documentBytes, docVm.DocumentFile.FileName, docVm.DocumentFile.ContentType);
|
||||||
|
fileName = docVm.DocumentFile.FileName;
|
||||||
|
fileSize = docVm.DocumentFile.Length;
|
||||||
|
uploadedAt = DateTime.UtcNow;
|
||||||
|
newFileIds.Add(fileId);
|
||||||
|
|
||||||
|
if (hasExisting && docVm.DocumentId != null)
|
||||||
|
removedFileIds.Add(docVm.DocumentId);
|
||||||
|
}
|
||||||
|
catch (ArgumentException ex)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError($"Documents[{index}].DocumentFile", ex.Message);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao salvar PDF do usuário");
|
||||||
|
ModelState.AddModelError($"Documents[{index}].DocumentFile", "Erro ao salvar o PDF. Tente novamente.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!hasExisting)
|
||||||
|
{
|
||||||
|
// Nenhum arquivo associado: ignorar
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(fileId))
|
||||||
|
{
|
||||||
|
ModelState.AddModelError($"Documents[{index}].DocumentFile", "Falha ao processar o documento.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
documents.Add(new PageDocument
|
||||||
|
{
|
||||||
|
Id = string.IsNullOrEmpty(docVm.Id) || !ObjectId.TryParse(docVm.Id, out _) ? ObjectId.GenerateNewId().ToString() : docVm.Id,
|
||||||
|
FileId = fileId,
|
||||||
|
Title = docVm.Title,
|
||||||
|
Description = docVm.Description,
|
||||||
|
FileName = fileName,
|
||||||
|
FileSize = fileSize,
|
||||||
|
UploadedAt = uploadedAt
|
||||||
|
});
|
||||||
|
|
||||||
|
docVm.DocumentId = fileId;
|
||||||
|
docVm.FileName = fileName;
|
||||||
|
docVm.FileSize = fileSize;
|
||||||
|
docVm.UploadedAt = uploadedAt;
|
||||||
|
if (string.IsNullOrEmpty(docVm.Id) || !ObjectId.TryParse(docVm.Id, out _))
|
||||||
|
{
|
||||||
|
docVm.Id = documents.Last().Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (planLimitations.MaxDocuments > 0 && documents.Count > planLimitations.MaxDocuments)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError("Documents", $"Você pode enviar no máximo {planLimitations.MaxDocuments} documento(s) com seu plano.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (documents, removedFileIds, newFileIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CleanupNewDocumentsAsync(IEnumerable<string> documentIds)
|
||||||
|
{
|
||||||
|
if (documentIds == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var docId in documentIds)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(docId))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _documentStorage.DeleteDocumentAsync(docId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Erro ao limpar documento temporário {DocumentId}", docId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateUserPageFromModel(UserPage page, ManagePageViewModel model, List<PageDocument> documents)
|
||||||
{
|
{
|
||||||
page.DisplayName = model.DisplayName;
|
page.DisplayName = model.DisplayName;
|
||||||
page.Category = model.Category;
|
page.Category = model.Category;
|
||||||
@ -1077,6 +1336,8 @@ public class AdminController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
page.Links.AddRange(socialLinks);
|
page.Links.AddRange(socialLinks);
|
||||||
|
|
||||||
|
page.Documents = documents ?? new List<PageDocument>();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
|
|||||||
64
src/BCards.Web/Controllers/DocumentController.cs
Normal file
64
src/BCards.Web/Controllers/DocumentController.cs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
using BCards.Web.Services;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace BCards.Web.Controllers;
|
||||||
|
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
public class DocumentController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IDocumentStorageService _documentStorage;
|
||||||
|
private readonly ILogger<DocumentController> _logger;
|
||||||
|
|
||||||
|
public DocumentController(IDocumentStorageService documentStorage, ILogger<DocumentController> logger)
|
||||||
|
{
|
||||||
|
_documentStorage = documentStorage;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{documentId}")]
|
||||||
|
[ResponseCache(Duration = 3600, Location = ResponseCacheLocation.Any)]
|
||||||
|
public async Task<IActionResult> GetDocument(string documentId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(documentId))
|
||||||
|
return BadRequest("Documento inválido.");
|
||||||
|
|
||||||
|
var documentBytes = await _documentStorage.GetDocumentAsync(documentId);
|
||||||
|
if (documentBytes == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
Response.Headers["Cache-Control"] = "public, max-age=31536000";
|
||||||
|
Response.Headers["Expires"] = DateTime.UtcNow.AddYears(1).ToString("R");
|
||||||
|
Response.Headers["ETag"] = $"\"{documentId}\"";
|
||||||
|
|
||||||
|
return File(documentBytes, "application/pdf");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao obter documento {DocumentId}", documentId);
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{documentId}")]
|
||||||
|
public async Task<IActionResult> DeleteDocument(string documentId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(documentId))
|
||||||
|
return BadRequest();
|
||||||
|
|
||||||
|
var deleted = await _documentStorage.DeleteDocumentAsync(documentId);
|
||||||
|
return deleted ? Ok(new { success = true }) : NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpHead("{documentId}")]
|
||||||
|
public async Task<IActionResult> DocumentExists(string documentId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(documentId))
|
||||||
|
return BadRequest();
|
||||||
|
|
||||||
|
var exists = await _documentStorage.DocumentExistsAsync(documentId);
|
||||||
|
return exists ? Ok() : NotFound();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -73,7 +73,7 @@ public class SubscriptionController : Controller
|
|||||||
{
|
{
|
||||||
case "immediate_with_refund":
|
case "immediate_with_refund":
|
||||||
success = await _paymentService.CancelSubscriptionImmediatelyAsync(request.SubscriptionId, true);
|
success = await _paymentService.CancelSubscriptionImmediatelyAsync(request.SubscriptionId, true);
|
||||||
message = success ? "Assinatura cancelada. Reembolso será processado manualmente em até 10 dias úteis." : "Erro ao processar cancelamento.";
|
message = success ? "Assinatura cancelada com reembolso total. O valor será retornado em até 10 dias úteis." : "Erro ao processar cancelamento.";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "immediate_no_refund":
|
case "immediate_no_refund":
|
||||||
@ -86,7 +86,7 @@ public class SubscriptionController : Controller
|
|||||||
var (_, canRefundPartial, refundAmount) = await _paymentService.CalculateRefundAsync(request.SubscriptionId);
|
var (_, canRefundPartial, refundAmount) = await _paymentService.CalculateRefundAsync(request.SubscriptionId);
|
||||||
if (success && canRefundPartial && refundAmount > 0)
|
if (success && canRefundPartial && refundAmount > 0)
|
||||||
{
|
{
|
||||||
message = $"Assinatura cancelada. Reembolso parcial de R$ {refundAmount:F2} será processado manualmente em até 10 dias úteis.";
|
message = $"Assinatura cancelada com reembolso parcial de R$ {refundAmount:F2}. O valor será retornado em até 10 dias úteis.";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@ -149,7 +149,9 @@ public class TestToolsController : ControllerBase
|
|||||||
AllowProductLinks = source.AllowProductLinks,
|
AllowProductLinks = source.AllowProductLinks,
|
||||||
SpecialModeration = source.SpecialModeration,
|
SpecialModeration = source.SpecialModeration,
|
||||||
OGExtractionsUsedToday = 0,
|
OGExtractionsUsedToday = 0,
|
||||||
LastExtractionDate = null
|
LastExtractionDate = null,
|
||||||
|
AllowDocumentUpload = source.AllowDocumentUpload,
|
||||||
|
MaxDocuments = source.MaxDocuments
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -90,7 +90,9 @@ public class PlanLimitationMiddleware
|
|||||||
AllowCustomDomain = false,
|
AllowCustomDomain = false,
|
||||||
AllowMultipleDomains = false,
|
AllowMultipleDomains = false,
|
||||||
PrioritySupport = false,
|
PrioritySupport = false,
|
||||||
PlanType = "basic"
|
PlanType = "basic",
|
||||||
|
AllowDocumentUpload = false,
|
||||||
|
MaxDocuments = 0
|
||||||
},
|
},
|
||||||
"professional" => new Models.PlanLimitations
|
"professional" => new Models.PlanLimitations
|
||||||
{
|
{
|
||||||
@ -100,7 +102,9 @@ public class PlanLimitationMiddleware
|
|||||||
AllowCustomDomain = true,
|
AllowCustomDomain = true,
|
||||||
AllowMultipleDomains = false,
|
AllowMultipleDomains = false,
|
||||||
PrioritySupport = false,
|
PrioritySupport = false,
|
||||||
PlanType = "professional"
|
PlanType = "professional",
|
||||||
|
AllowDocumentUpload = false,
|
||||||
|
MaxDocuments = 0
|
||||||
},
|
},
|
||||||
"premium" => new Models.PlanLimitations
|
"premium" => new Models.PlanLimitations
|
||||||
{
|
{
|
||||||
@ -110,7 +114,21 @@ public class PlanLimitationMiddleware
|
|||||||
AllowCustomDomain = true,
|
AllowCustomDomain = true,
|
||||||
AllowMultipleDomains = true,
|
AllowMultipleDomains = true,
|
||||||
PrioritySupport = true,
|
PrioritySupport = true,
|
||||||
PlanType = "premium"
|
PlanType = "premium",
|
||||||
|
AllowDocumentUpload = true,
|
||||||
|
MaxDocuments = 5
|
||||||
|
},
|
||||||
|
"premiumaffiliate" => new Models.PlanLimitations
|
||||||
|
{
|
||||||
|
MaxLinks = -1,
|
||||||
|
AllowCustomThemes = true,
|
||||||
|
AllowAnalytics = true,
|
||||||
|
AllowCustomDomain = true,
|
||||||
|
AllowMultipleDomains = true,
|
||||||
|
PrioritySupport = true,
|
||||||
|
PlanType = "premiumaffiliate",
|
||||||
|
AllowDocumentUpload = true,
|
||||||
|
MaxDocuments = 10
|
||||||
},
|
},
|
||||||
_ => new Models.PlanLimitations
|
_ => new Models.PlanLimitations
|
||||||
{
|
{
|
||||||
@ -120,7 +138,9 @@ public class PlanLimitationMiddleware
|
|||||||
AllowCustomDomain = false,
|
AllowCustomDomain = false,
|
||||||
AllowMultipleDomains = false,
|
AllowMultipleDomains = false,
|
||||||
PrioritySupport = false,
|
PrioritySupport = false,
|
||||||
PlanType = "free"
|
PlanType = "free",
|
||||||
|
AllowDocumentUpload = false,
|
||||||
|
MaxDocuments = 0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
string BusinessType { get; }
|
string BusinessType { get; }
|
||||||
PageTheme Theme { get; }
|
PageTheme Theme { get; }
|
||||||
List<LinkItem> Links { get; }
|
List<LinkItem> Links { get; }
|
||||||
|
List<PageDocument> Documents { get; }
|
||||||
SeoSettings SeoSettings { get; }
|
SeoSettings SeoSettings { get; }
|
||||||
string Language { get; }
|
string Language { get; }
|
||||||
DateTime CreatedAt { get; }
|
DateTime CreatedAt { get; }
|
||||||
|
|||||||
@ -47,6 +47,9 @@ public class LivePage : IPageDisplay
|
|||||||
[BsonElement("links")]
|
[BsonElement("links")]
|
||||||
public List<LinkItem> Links { get; set; } = new();
|
public List<LinkItem> Links { get; set; } = new();
|
||||||
|
|
||||||
|
[BsonElement("documents")]
|
||||||
|
public List<PageDocument> Documents { get; set; } = new();
|
||||||
|
|
||||||
[BsonElement("seoSettings")]
|
[BsonElement("seoSettings")]
|
||||||
public SeoSettings SeoSettings { get; set; } = new();
|
public SeoSettings SeoSettings { get; set; } = new();
|
||||||
|
|
||||||
|
|||||||
30
src/BCards.Web/Models/PageDocument.cs
Normal file
30
src/BCards.Web/Models/PageDocument.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
using MongoDB.Bson;
|
||||||
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
|
|
||||||
|
namespace BCards.Web.Models;
|
||||||
|
|
||||||
|
public class PageDocument
|
||||||
|
{
|
||||||
|
[BsonId]
|
||||||
|
[BsonRepresentation(BsonType.ObjectId)]
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[BsonElement("fileId")]
|
||||||
|
[BsonRepresentation(BsonType.ObjectId)]
|
||||||
|
public string FileId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[BsonElement("title")]
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[BsonElement("description")]
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[BsonElement("fileName")]
|
||||||
|
public string FileName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[BsonElement("fileSize")]
|
||||||
|
public long FileSize { get; set; }
|
||||||
|
|
||||||
|
[BsonElement("uploadedAt")]
|
||||||
|
public DateTime UploadedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
@ -43,4 +43,10 @@ public class PlanLimitations
|
|||||||
|
|
||||||
[BsonElement("lastExtractionDate")]
|
[BsonElement("lastExtractionDate")]
|
||||||
public DateTime? LastExtractionDate { get; set; }
|
public DateTime? LastExtractionDate { get; set; }
|
||||||
|
|
||||||
|
[BsonElement("allowDocumentUpload")]
|
||||||
|
public bool AllowDocumentUpload { get; set; } = false;
|
||||||
|
|
||||||
|
[BsonElement("maxDocuments")]
|
||||||
|
public int MaxDocuments { get; set; } = 0;
|
||||||
}
|
}
|
||||||
@ -31,10 +31,10 @@ public static class PlanTypeExtensions
|
|||||||
return planType switch
|
return planType switch
|
||||||
{
|
{
|
||||||
PlanType.Trial => 0.00m,
|
PlanType.Trial => 0.00m,
|
||||||
PlanType.Basic => 5.90m,
|
PlanType.Basic => 12.90m,
|
||||||
PlanType.Professional => 12.90m,
|
PlanType.Professional => 25.90m,
|
||||||
PlanType.Premium => 19.90m,
|
PlanType.Premium => 29.90m,
|
||||||
PlanType.PremiumAffiliate => 29.90m,
|
PlanType.PremiumAffiliate => 34.90m,
|
||||||
_ => 0.00m
|
_ => 0.00m
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,6 +44,9 @@ public class UserPage : IPageDisplay
|
|||||||
[BsonElement("links")]
|
[BsonElement("links")]
|
||||||
public List<LinkItem> Links { get; set; } = new();
|
public List<LinkItem> Links { get; set; } = new();
|
||||||
|
|
||||||
|
[BsonElement("documents")]
|
||||||
|
public List<PageDocument> Documents { get; set; } = new();
|
||||||
|
|
||||||
[BsonElement("seoSettings")]
|
[BsonElement("seoSettings")]
|
||||||
public SeoSettings SeoSettings { get; set; } = new();
|
public SeoSettings SeoSettings { get; set; } = new();
|
||||||
|
|
||||||
|
|||||||
@ -483,6 +483,7 @@ builder.Services.AddScoped<IEmailService, EmailService>();
|
|||||||
builder.Services.AddScoped<IDowngradeService, DowngradeService>();
|
builder.Services.AddScoped<IDowngradeService, DowngradeService>();
|
||||||
|
|
||||||
builder.Services.AddScoped<IImageStorageService, GridFSImageStorage>();
|
builder.Services.AddScoped<IImageStorageService, GridFSImageStorage>();
|
||||||
|
builder.Services.AddScoped<IDocumentStorageService, GridFSDocumentStorage>();
|
||||||
|
|
||||||
// Support Area - Rating and Contact System
|
// Support Area - Rating and Contact System
|
||||||
builder.Services.Configure<BCards.Web.Configuration.SupportSettings>(
|
builder.Services.Configure<BCards.Web.Configuration.SupportSettings>(
|
||||||
@ -492,23 +493,27 @@ builder.Services.AddScoped<BCards.Web.Areas.Support.Repositories.IRatingReposito
|
|||||||
builder.Services.AddScoped<BCards.Web.Areas.Support.Services.IRatingService, BCards.Web.Areas.Support.Services.RatingService>();
|
builder.Services.AddScoped<BCards.Web.Areas.Support.Services.IRatingService, BCards.Web.Areas.Support.Services.RatingService>();
|
||||||
builder.Services.AddScoped<BCards.Web.Areas.Support.Services.ISupportService, BCards.Web.Areas.Support.Services.SupportService>();
|
builder.Services.AddScoped<BCards.Web.Areas.Support.Services.ISupportService, BCards.Web.Areas.Support.Services.SupportService>();
|
||||||
|
|
||||||
|
// Markdown/Articles System - Tutoriais e Artigos Areas
|
||||||
|
builder.Services.AddScoped<BCards.Web.Areas.Tutoriais.Services.IMarkdownService, BCards.Web.Areas.Tutoriais.Services.MarkdownService>();
|
||||||
|
|
||||||
// Configure upload limits for file handling (images up to 5MB)
|
// Configure upload limits for file handling (images up to 5MB)
|
||||||
builder.Services.Configure<FormOptions>(options =>
|
builder.Services.Configure<FormOptions>(options =>
|
||||||
{
|
{
|
||||||
options.MultipartBodyLengthLimit = 5 * 1024 * 1024; // 5MB
|
var maxUploadSize = 12 * 1024 * 1024; // 12MB
|
||||||
|
options.MultipartBodyLengthLimit = maxUploadSize;
|
||||||
options.ValueLengthLimit = int.MaxValue;
|
options.ValueLengthLimit = int.MaxValue;
|
||||||
options.ValueCountLimit = int.MaxValue;
|
options.ValueCountLimit = int.MaxValue;
|
||||||
options.KeyLengthLimit = int.MaxValue;
|
options.KeyLengthLimit = int.MaxValue;
|
||||||
options.BufferBody = true;
|
options.BufferBody = true;
|
||||||
options.BufferBodyLengthLimit = 5 * 1024 * 1024; // 5MB
|
options.BufferBodyLengthLimit = maxUploadSize;
|
||||||
options.MultipartBodyLengthLimit = 5 * 1024 * 1024; // 5MB
|
options.MultipartBodyLengthLimit = maxUploadSize;
|
||||||
options.MultipartHeadersLengthLimit = 16384;
|
options.MultipartHeadersLengthLimit = 16384;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Configure Kestrel server limits for larger requests
|
// Configure Kestrel server limits for larger requests
|
||||||
builder.Services.Configure<Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions>(options =>
|
builder.Services.Configure<Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions>(options =>
|
||||||
{
|
{
|
||||||
options.Limits.MaxRequestBodySize = 5 * 1024 * 1024; // 5MB
|
options.Limits.MaxRequestBodySize = 12 * 1024 * 1024; // 12MB
|
||||||
options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(2);
|
options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(2);
|
||||||
options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
|
options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
|
||||||
});
|
});
|
||||||
@ -786,6 +791,56 @@ app.MapControllerRoute(
|
|||||||
pattern: "terminos",
|
pattern: "terminos",
|
||||||
defaults: new { controller = "Legal", action = "TermsES" });
|
defaults: new { controller = "Legal", action = "TermsES" });
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// AREAS: Tutoriais e Artigos
|
||||||
|
// ========================================
|
||||||
|
// IMPORTANTE: Ordem importa! Rotas mais específicas primeiro.
|
||||||
|
|
||||||
|
// Artigos Area - Índice (ANTES para evitar conflito)
|
||||||
|
app.MapAreaControllerRoute(
|
||||||
|
name: "artigos-index",
|
||||||
|
areaName: "Artigos",
|
||||||
|
pattern: "artigos",
|
||||||
|
defaults: new { controller = "Artigos", action = "Index" });
|
||||||
|
|
||||||
|
// Artigos Area - Artigo específico
|
||||||
|
app.MapAreaControllerRoute(
|
||||||
|
name: "artigos-article",
|
||||||
|
areaName: "Artigos",
|
||||||
|
pattern: "artigos/{slug}",
|
||||||
|
defaults: new { controller = "Artigos", action = "Article" },
|
||||||
|
constraints: new { slug = @"^[a-z0-9\-]+$" });
|
||||||
|
|
||||||
|
// Tutoriais Area - Índice
|
||||||
|
app.MapAreaControllerRoute(
|
||||||
|
name: "tutoriais-index",
|
||||||
|
areaName: "Tutoriais",
|
||||||
|
pattern: "tutoriais",
|
||||||
|
defaults: new { controller = "Tutoriais", action = "Index" });
|
||||||
|
|
||||||
|
// Tutoriais Area - Lista de artigos por categoria
|
||||||
|
app.MapAreaControllerRoute(
|
||||||
|
name: "tutoriais-category",
|
||||||
|
areaName: "Tutoriais",
|
||||||
|
pattern: "tutoriais/{categoria}",
|
||||||
|
defaults: new { controller = "Tutoriais", action = "Category" },
|
||||||
|
constraints: new { categoria = @"^[a-z\-]+$" });
|
||||||
|
|
||||||
|
// Tutoriais Area - Artigo específico
|
||||||
|
app.MapAreaControllerRoute(
|
||||||
|
name: "tutoriais-article",
|
||||||
|
areaName: "Tutoriais",
|
||||||
|
pattern: "tutoriais/{categoria}/{slug}",
|
||||||
|
defaults: new { controller = "Tutoriais", action = "Article" },
|
||||||
|
constraints: new
|
||||||
|
{
|
||||||
|
categoria = @"^[a-z\-]+$", // slug de categoria
|
||||||
|
slug = @"^[a-z0-9\-]+$" // slug do artigo
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Rota default
|
||||||
|
// ========================================
|
||||||
app.MapControllerRoute(
|
app.MapControllerRoute(
|
||||||
name: "default",
|
name: "default",
|
||||||
pattern: "{controller=Home}/{action=Index}/{id?}");
|
pattern: "{controller=Home}/{action=Index}/{id?}");
|
||||||
|
|||||||
93
src/BCards.Web/Services/GridFSDocumentStorage.cs
Normal file
93
src/BCards.Web/Services/GridFSDocumentStorage.cs
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
using MongoDB.Bson;
|
||||||
|
using MongoDB.Driver;
|
||||||
|
using MongoDB.Driver.GridFS;
|
||||||
|
|
||||||
|
namespace BCards.Web.Services;
|
||||||
|
|
||||||
|
public class GridFSDocumentStorage : IDocumentStorageService
|
||||||
|
{
|
||||||
|
private const int MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
||||||
|
private static readonly string[] ALLOWED_TYPES = { "application/pdf" };
|
||||||
|
|
||||||
|
private readonly GridFSBucket _gridFS;
|
||||||
|
private readonly ILogger<GridFSDocumentStorage> _logger;
|
||||||
|
|
||||||
|
public GridFSDocumentStorage(IMongoDatabase database, ILogger<GridFSDocumentStorage> logger)
|
||||||
|
{
|
||||||
|
_gridFS = new GridFSBucket(database, new GridFSBucketOptions
|
||||||
|
{
|
||||||
|
BucketName = "page_documents"
|
||||||
|
});
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> SaveDocumentAsync(byte[] documentBytes, string fileName, string contentType)
|
||||||
|
{
|
||||||
|
if (documentBytes == null || documentBytes.Length == 0)
|
||||||
|
throw new ArgumentException("Documento inválido.");
|
||||||
|
|
||||||
|
if (documentBytes.Length > MAX_FILE_SIZE)
|
||||||
|
throw new ArgumentException($"Arquivo muito grande. Tamanho máximo permitido: {MAX_FILE_SIZE / (1024 * 1024)}MB.");
|
||||||
|
|
||||||
|
if (!ALLOWED_TYPES.Contains(contentType.ToLower()))
|
||||||
|
throw new ArgumentException("Tipo de arquivo não suportado. Envie um PDF.");
|
||||||
|
|
||||||
|
var uniqueFileName = $"document_{DateTime.UtcNow:yyyyMMdd_HHmmss}_{Guid.NewGuid():N}.pdf";
|
||||||
|
|
||||||
|
var options = new GridFSUploadOptions
|
||||||
|
{
|
||||||
|
Metadata = new BsonDocument
|
||||||
|
{
|
||||||
|
{ "originalFileName", fileName },
|
||||||
|
{ "contentType", contentType },
|
||||||
|
{ "uploadDate", DateTime.UtcNow },
|
||||||
|
{ "size", documentBytes.Length }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var fileId = await _gridFS.UploadFromBytesAsync(uniqueFileName, documentBytes, options);
|
||||||
|
_logger.LogInformation("PDF salvo no GridFS: {FileId}", fileId);
|
||||||
|
return fileId.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<byte[]?> GetDocumentAsync(string documentId)
|
||||||
|
{
|
||||||
|
if (!ObjectId.TryParse(documentId, out var objectId))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _gridFS.DownloadAsBytesAsync(objectId);
|
||||||
|
}
|
||||||
|
catch (GridFSFileNotFoundException)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteDocumentAsync(string documentId)
|
||||||
|
{
|
||||||
|
if (!ObjectId.TryParse(documentId, out var objectId))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _gridFS.DeleteAsync(objectId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (GridFSFileNotFoundException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DocumentExistsAsync(string documentId)
|
||||||
|
{
|
||||||
|
if (!ObjectId.TryParse(documentId, out var objectId))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var filter = Builders<GridFSFileInfo>.Filter.Eq("_id", objectId);
|
||||||
|
var fileInfo = await _gridFS.Find(filter).FirstOrDefaultAsync();
|
||||||
|
return fileInfo != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/BCards.Web/Services/IDocumentStorageService.cs
Normal file
9
src/BCards.Web/Services/IDocumentStorageService.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace BCards.Web.Services;
|
||||||
|
|
||||||
|
public interface IDocumentStorageService
|
||||||
|
{
|
||||||
|
Task<string> SaveDocumentAsync(byte[] documentBytes, string fileName, string contentType);
|
||||||
|
Task<byte[]?> GetDocumentAsync(string documentId);
|
||||||
|
Task<bool> DeleteDocumentAsync(string documentId);
|
||||||
|
Task<bool> DocumentExistsAsync(string documentId);
|
||||||
|
}
|
||||||
@ -51,6 +51,8 @@ public class PlanConfiguration
|
|||||||
public bool AllowProductLinks { get; set; }
|
public bool AllowProductLinks { get; set; }
|
||||||
public bool AllowAnalytics { get; set; }
|
public bool AllowAnalytics { get; set; }
|
||||||
public bool? SpecialModeration { get; set; }
|
public bool? SpecialModeration { get; set; }
|
||||||
|
public bool AllowDocumentUpload { get; set; }
|
||||||
|
public int MaxDocuments { get; set; }
|
||||||
public List<string> Features { get; set; } = new();
|
public List<string> Features { get; set; } = new();
|
||||||
public string Interval { get; set; } = "month";
|
public string Interval { get; set; } = "month";
|
||||||
public PlanType BasePlanType { get; set; }
|
public PlanType BasePlanType { get; set; }
|
||||||
|
|||||||
@ -59,6 +59,7 @@ public class LivePageService : ILivePageService
|
|||||||
BusinessType = userPage.BusinessType,
|
BusinessType = userPage.BusinessType,
|
||||||
Theme = userPage.Theme,
|
Theme = userPage.Theme,
|
||||||
Links = userPage.Links,
|
Links = userPage.Links,
|
||||||
|
Documents = userPage.Documents,
|
||||||
SeoSettings = userPage.SeoSettings,
|
SeoSettings = userPage.SeoSettings,
|
||||||
Language = userPage.Language,
|
Language = userPage.Language,
|
||||||
Analytics = new LivePageAnalytics
|
Analytics = new LivePageAnalytics
|
||||||
|
|||||||
@ -372,17 +372,46 @@ public class PaymentService : IPaymentService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var service = new SubscriptionService();
|
var service = new SubscriptionService();
|
||||||
|
var subscription = await service.GetAsync(subscriptionId);
|
||||||
|
|
||||||
if (refund)
|
if (refund)
|
||||||
{
|
{
|
||||||
// Para reembolso completo, apenas cancela - reembolso deve ser feito manualmente via Stripe Dashboard
|
// Processar reembolso automático - obter última charge e reembolsá-la
|
||||||
await service.CancelAsync(subscriptionId);
|
try
|
||||||
}
|
{
|
||||||
else
|
var chargeService = new ChargeService();
|
||||||
{
|
var charges = await chargeService.ListAsync(new ChargeListOptions
|
||||||
await service.CancelAsync(subscriptionId);
|
{
|
||||||
|
Customer = subscription.CustomerId,
|
||||||
|
Limit = 1
|
||||||
|
});
|
||||||
|
|
||||||
|
if (charges.Data.Any())
|
||||||
|
{
|
||||||
|
var lastCharge = charges.Data.First();
|
||||||
|
if (lastCharge.Refunded == false && !string.IsNullOrEmpty(lastCharge.Id))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var refundService = new RefundService();
|
||||||
|
await refundService.CreateAsync(new RefundCreateOptions { Charge = lastCharge.Id });
|
||||||
|
}
|
||||||
|
catch (StripeException)
|
||||||
|
{
|
||||||
|
// Reembolso falhou mas continuaremos com o cancelamento
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (StripeException)
|
||||||
|
{
|
||||||
|
// Se houver erro ao obter charge, ainda cancela a assinatura
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cancelar a assinatura
|
||||||
|
await service.CancelAsync(subscriptionId);
|
||||||
|
|
||||||
// Atualizar subscription local
|
// Atualizar subscription local
|
||||||
var localSubscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(subscriptionId);
|
var localSubscription = await _subscriptionRepository.GetByStripeSubscriptionIdAsync(subscriptionId);
|
||||||
if (localSubscription != null)
|
if (localSubscription != null)
|
||||||
|
|||||||
@ -47,7 +47,9 @@ public class PlanConfigurationService : IPlanConfigurationService
|
|||||||
PrioritySupport = false,
|
PrioritySupport = false,
|
||||||
AllowProductLinks = false,
|
AllowProductLinks = false,
|
||||||
MaxProductLinks = 0,
|
MaxProductLinks = 0,
|
||||||
PlanType = "trial"
|
PlanType = "trial",
|
||||||
|
AllowDocumentUpload = false,
|
||||||
|
MaxDocuments = 0
|
||||||
},
|
},
|
||||||
PlanType.Basic => new PlanLimitations
|
PlanType.Basic => new PlanLimitations
|
||||||
{
|
{
|
||||||
@ -59,7 +61,9 @@ public class PlanConfigurationService : IPlanConfigurationService
|
|||||||
PrioritySupport = false,
|
PrioritySupport = false,
|
||||||
AllowProductLinks = GetConfigValue(PlanType.Basic, "AllowProductLinks", false),
|
AllowProductLinks = GetConfigValue(PlanType.Basic, "AllowProductLinks", false),
|
||||||
MaxProductLinks = 0,
|
MaxProductLinks = 0,
|
||||||
PlanType = "basic"
|
PlanType = "basic",
|
||||||
|
AllowDocumentUpload = GetConfigValue(PlanType.Basic, "AllowDocumentUpload", false),
|
||||||
|
MaxDocuments = GetConfigValue(PlanType.Basic, "MaxDocuments", 0)
|
||||||
},
|
},
|
||||||
PlanType.Professional => new PlanLimitations
|
PlanType.Professional => new PlanLimitations
|
||||||
{
|
{
|
||||||
@ -71,7 +75,9 @@ public class PlanConfigurationService : IPlanConfigurationService
|
|||||||
PrioritySupport = false,
|
PrioritySupport = false,
|
||||||
AllowProductLinks = GetConfigValue(PlanType.Professional, "AllowProductLinks", false),
|
AllowProductLinks = GetConfigValue(PlanType.Professional, "AllowProductLinks", false),
|
||||||
MaxProductLinks = 0,
|
MaxProductLinks = 0,
|
||||||
PlanType = "professional"
|
PlanType = "professional",
|
||||||
|
AllowDocumentUpload = GetConfigValue(PlanType.Professional, "AllowDocumentUpload", false),
|
||||||
|
MaxDocuments = GetConfigValue(PlanType.Professional, "MaxDocuments", 0)
|
||||||
},
|
},
|
||||||
PlanType.Premium => new PlanLimitations
|
PlanType.Premium => new PlanLimitations
|
||||||
{
|
{
|
||||||
@ -83,7 +89,9 @@ public class PlanConfigurationService : IPlanConfigurationService
|
|||||||
PrioritySupport = true,
|
PrioritySupport = true,
|
||||||
AllowProductLinks = GetConfigValue(PlanType.Premium, "AllowProductLinks", false),
|
AllowProductLinks = GetConfigValue(PlanType.Premium, "AllowProductLinks", false),
|
||||||
MaxProductLinks = 0,
|
MaxProductLinks = 0,
|
||||||
PlanType = "premium"
|
PlanType = "premium",
|
||||||
|
AllowDocumentUpload = GetConfigValue(PlanType.Premium, "AllowDocumentUpload", true),
|
||||||
|
MaxDocuments = GetConfigValue(PlanType.Premium, "MaxDocuments", 5)
|
||||||
},
|
},
|
||||||
PlanType.PremiumAffiliate => new PlanLimitations
|
PlanType.PremiumAffiliate => new PlanLimitations
|
||||||
{
|
{
|
||||||
@ -95,9 +103,11 @@ public class PlanConfigurationService : IPlanConfigurationService
|
|||||||
PrioritySupport = true,
|
PrioritySupport = true,
|
||||||
AllowProductLinks = GetConfigValue(PlanType.PremiumAffiliate, "AllowProductLinks", true),
|
AllowProductLinks = GetConfigValue(PlanType.PremiumAffiliate, "AllowProductLinks", true),
|
||||||
MaxProductLinks = 10,
|
MaxProductLinks = 10,
|
||||||
PlanType = "premiumaffiliate"
|
PlanType = "premiumaffiliate",
|
||||||
|
AllowDocumentUpload = GetConfigValue(PlanType.PremiumAffiliate, "AllowDocumentUpload", true),
|
||||||
|
MaxDocuments = GetConfigValue(PlanType.PremiumAffiliate, "MaxDocuments", 10)
|
||||||
},
|
},
|
||||||
_ => new PlanLimitations { PlanType = "trial" }
|
_ => new PlanLimitations { PlanType = "trial", AllowDocumentUpload = false, MaxDocuments = 0 }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -178,8 +178,8 @@ public class TrialExpirationService : BackgroundService
|
|||||||
|
|
||||||
Para continuar usando sua página de links, escolha um de nossos planos:
|
Para continuar usando sua página de links, escolha um de nossos planos:
|
||||||
|
|
||||||
• Básico - R$ 9,90/mês
|
• Básico - R$ 12,90/mês
|
||||||
• Profissional - R$ 24,90/mês
|
• Profissional - R$ 25,90/mês
|
||||||
• Premium - R$ 29,90/mês
|
• Premium - R$ 29,90/mês
|
||||||
|
|
||||||
Acesse: {GetUpgradeUrl()}
|
Acesse: {GetUpgradeUrl()}
|
||||||
@ -204,9 +204,9 @@ public class TrialExpirationService : BackgroundService
|
|||||||
|
|
||||||
Para reativar sua página, escolha um de nossos planos:
|
Para reativar sua página, escolha um de nossos planos:
|
||||||
|
|
||||||
• Básico - R$ 9,90/mês - 5 links, analytics básicos
|
• Básico - R$ 12,90/mês - 5 links, analytics básicos
|
||||||
• Profissional - R$ 24,90/mês - 15 links, todos os temas, analytics avançados
|
• Profissional - R$ 25,90/mês - 15 links, todos os temas, analytics avançados
|
||||||
• Premium - R$ 29,90/mês - Links ilimitados, temas premium, analytics completos
|
• Premium - R$ 29,90/mês - Links ilimitados, temas premium, analytics completos, upload de PDFs
|
||||||
|
|
||||||
Seus dados estão seguros e serão restaurados assim que você escolher um plano.
|
Seus dados estão seguros e serão restaurados assim que você escolher um plano.
|
||||||
|
|
||||||
|
|||||||
@ -43,6 +43,7 @@ public class ManagePageViewModel
|
|||||||
public string KawaiUrl { get; set; } = string.Empty;
|
public string KawaiUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
public List<ManageLinkViewModel> Links { get; set; } = new();
|
public List<ManageLinkViewModel> Links { get; set; } = new();
|
||||||
|
public List<ManageDocumentViewModel> Documents { get; set; } = new();
|
||||||
|
|
||||||
// Profile image fields
|
// Profile image fields
|
||||||
public string? ProfileImageId { get; set; }
|
public string? ProfileImageId { get; set; }
|
||||||
@ -55,6 +56,9 @@ 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 AllowProductLinks { get; set; } = false;
|
||||||
|
public int MaxDocumentsAllowed { get; set; } = 0;
|
||||||
|
public bool AllowDocumentUpload { get; set; } = false;
|
||||||
|
public string DocumentUploadPlansDisplay { get; set; } = "planos com suporte a documentos";
|
||||||
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>
|
||||||
@ -101,6 +105,28 @@ public class ManageLinkViewModel
|
|||||||
public DateTime? ProductDataCachedAt { get; set; }
|
public DateTime? ProductDataCachedAt { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class ManageDocumentViewModel
|
||||||
|
{
|
||||||
|
// Campos opcionais - preenchidos pelo model binding ou pelo controller
|
||||||
|
public string? Id { get; set; }
|
||||||
|
public string? DocumentId { get; set; }
|
||||||
|
public string? FileName { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "Título é obrigatório")]
|
||||||
|
[StringLength(120, ErrorMessage = "Título deve ter no máximo 120 caracteres")]
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[StringLength(300, ErrorMessage = "Descrição deve ter no máximo 300 caracteres")]
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public long FileSize { get; set; }
|
||||||
|
public DateTime? UploadedAt { get; set; }
|
||||||
|
|
||||||
|
public IFormFile? DocumentFile { get; set; }
|
||||||
|
public bool MarkForRemoval { get; set; }
|
||||||
|
public bool ReplaceExisting => DocumentFile != null && !string.IsNullOrEmpty(DocumentId);
|
||||||
|
}
|
||||||
|
|
||||||
public class DashboardViewModel
|
public class DashboardViewModel
|
||||||
{
|
{
|
||||||
public User CurrentUser { get; set; } = new();
|
public User CurrentUser { get; set; } = new();
|
||||||
|
|||||||
@ -29,7 +29,7 @@
|
|||||||
<div class="progress-bar" role="progressbar" style="width: 25%" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100"></div>
|
<div class="progress-bar" role="progressbar" style="width: 25%" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-between mt-2">
|
<div class="d-flex justify-content-between mt-2">
|
||||||
<small class="text-muted">Passo 1 de 4</small>
|
<small class="text-muted">Passo 1 de 5</small>
|
||||||
<small class="text-muted">Informações Básicas</small>
|
<small class="text-muted">Informações Básicas</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -224,17 +224,158 @@
|
|||||||
Próximo <i class="fas fa-arrow-right ms-1"></i>
|
Próximo <i class="fas fa-arrow-right ms-1"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Passo 3: Documentos PDF (Premium) -->
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header" id="headingDocuments">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseDocuments" aria-expanded="false" aria-controls="collapseDocuments">
|
||||||
|
<i class="fas fa-file-pdf me-2"></i>
|
||||||
|
Passo 3: Documentos PDF (Premium)
|
||||||
|
<span class="badge bg-success ms-auto me-3" id="step3Status" style="display: none;">✓</span>
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="collapseDocuments" class="accordion-collapse collapse" aria-labelledby="headingDocuments" data-bs-parent="#pageWizard">
|
||||||
|
<div class="accordion-body">
|
||||||
|
@if (Model.AllowDocumentUpload)
|
||||||
|
{
|
||||||
|
<p class="text-muted mb-3">Anexe PDFs com apresentações, catálogos ou materiais exclusivos para quem acessar sua página Premium.</p>
|
||||||
|
|
||||||
|
@if (Model.MaxDocumentsAllowed > 0)
|
||||||
|
{
|
||||||
|
<div class="alert alert-light border-start border-3 border-primary py-2 small">
|
||||||
|
<i class="fas fa-info-circle me-2 text-primary"></i>
|
||||||
|
Você pode anexar até <strong>@Model.MaxDocumentsAllowed</strong> documento(s) no seu plano atual.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div id="documentsContainer">
|
||||||
|
@if (Model.Documents != null && Model.Documents.Count > 0)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < Model.Documents.Count; i++)
|
||||||
|
{
|
||||||
|
<div class="document-input-group border rounded p-3 mb-3" data-document="@i">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-1">Documento @(i + 1)</h6>
|
||||||
|
@if (Model.Documents[i].UploadedAt.HasValue)
|
||||||
|
{
|
||||||
|
<small class="text-muted">Atualizado em @Model.Documents[i].UploadedAt.Value.ToLocalTime().ToString("dd/MM/yyyy HH:mm")</small>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
@if (!string.IsNullOrEmpty(Model.Documents[i].DocumentId))
|
||||||
|
{
|
||||||
|
<a class="btn btn-sm btn-outline-primary" href="/api/document/@Model.Documents[i].DocumentId" target="_blank">
|
||||||
|
<i class="fas fa-file-pdf me-1"></i> Ver PDF
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger remove-document-btn">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Título <span class="text-danger">*</span></label>
|
||||||
|
<input asp-for="Documents[i].Title" class="form-control" placeholder="Ex: Apresentação de Serviços">
|
||||||
|
<span asp-validation-for="Documents[i].Title" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Descrição (opcional)</label>
|
||||||
|
<textarea asp-for="Documents[i].Description" class="form-control" rows="2" placeholder="Resumo do conteúdo"></textarea>
|
||||||
|
<span asp-validation-for="Documents[i].Description" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Arquivo PDF</label>
|
||||||
|
@if (!string.IsNullOrEmpty(Model.Documents[i].DocumentId))
|
||||||
|
{
|
||||||
|
<div class="bg-light border rounded p-3 mb-2 document-file-info">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<strong>@Model.Documents[i].FileName</strong>
|
||||||
|
<div class="small text-muted">@((Model.Documents[i].FileSize / 1024.0).ToString("0.#")) KB</div>
|
||||||
|
</div>
|
||||||
|
<span class="badge bg-primary-subtle text-primary">PDF</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input asp-for="Documents[i].DocumentFile" class="form-control" type="file" accept="application/pdf">
|
||||||
|
<small class="form-text text-muted">Envie outro PDF para substituir o arquivo atual (máx. 10MB).</small>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<input asp-for="Documents[i].DocumentFile" class="form-control" type="file" accept="application/pdf">
|
||||||
|
<small class="form-text text-muted">Envie um arquivo PDF (máx. 10MB).</small>
|
||||||
|
}
|
||||||
|
<span asp-validation-for="Documents[i].DocumentFile" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input asp-for="Documents[i].Id" type="hidden">
|
||||||
|
<input asp-for="Documents[i].DocumentId" type="hidden">
|
||||||
|
<input asp-for="Documents[i].FileName" type="hidden">
|
||||||
|
<input asp-for="Documents[i].FileSize" type="hidden">
|
||||||
|
<input asp-for="Documents[i].UploadedAt" type="hidden">
|
||||||
|
<input asp-for="Documents[i].MarkForRemoval" type="hidden">
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="alert alert-info" id="documentsEmptyState" style="display: none;">
|
||||||
|
<i class="fas fa-folder-open me-2"></i>Nenhum documento adicionado ainda.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="alert alert-info" id="documentsEmptyState">
|
||||||
|
<i class="fas fa-folder-open me-2"></i>Nenhum documento adicionado ainda.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mt-3">
|
||||||
|
<small class="text-muted">Os documentos são exibidos em ordem de cadastro. Utilize títulos claros para facilitar o acesso.</small>
|
||||||
|
<button type="button" class="btn btn-outline-primary" id="addDocumentBtn">
|
||||||
|
<i class="fas fa-plus me-2"></i>Adicionar Documento
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="alert alert-warning d-flex align-items-start">
|
||||||
|
<i class="fas fa-crown me-2 mt-1"></i>
|
||||||
|
<div>
|
||||||
|
Upload de PDFs disponível apenas nos planos <strong>@Model.DocumentUploadPlansDisplay</strong>.
|
||||||
|
<div class="mt-2">
|
||||||
|
<a asp-controller="Home" asp-action="Pricing" class="btn btn-warning btn-sm">
|
||||||
|
<i class="fas fa-arrow-up me-1"></i>Fazer upgrade agora
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between mt-4">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" onclick="previousStep(2)">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i> Anterior
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="nextStep(4)">
|
||||||
|
Próximo <i class="fas fa-arrow-right ms-1"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Passo 3: Links Principais -->
|
<!-- Passo 4: Links Principais -->
|
||||||
<div class="accordion-item">
|
<div class="accordion-item">
|
||||||
<h2 class="accordion-header" id="headingLinks">
|
<h2 class="accordion-header" id="headingLinks">
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseLinks" aria-expanded="false" aria-controls="collapseLinks">
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseLinks" aria-expanded="false" aria-controls="collapseLinks">
|
||||||
<i class="fas fa-link me-2"></i>
|
<i class="fas fa-link me-2"></i>
|
||||||
Passo 3: Links Principais
|
Passo 4: Links Principais
|
||||||
<span class="badge bg-success ms-auto me-3" id="step3Status" style="display: none;">✓</span>
|
<span class="badge bg-success ms-auto me-3" id="step4Status" style="display: none;">✓</span>
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="collapseLinks" class="accordion-collapse collapse" aria-labelledby="headingLinks" data-bs-parent="#pageWizard">
|
<div id="collapseLinks" class="accordion-collapse collapse" aria-labelledby="headingLinks" data-bs-parent="#pageWizard">
|
||||||
@ -371,10 +512,10 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<button type="button" class="btn btn-outline-secondary" onclick="previousStep(2)">
|
<button type="button" class="btn btn-outline-secondary" onclick="previousStep(3)">
|
||||||
<i class="fas fa-arrow-left me-1"></i> Anterior
|
<i class="fas fa-arrow-left me-1"></i> Anterior
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-primary" onclick="nextStep(4)">
|
<button type="button" class="btn btn-primary" onclick="nextStep(5)">
|
||||||
Próximo <i class="fas fa-arrow-right ms-1"></i>
|
Próximo <i class="fas fa-arrow-right ms-1"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -394,18 +535,18 @@
|
|||||||
var twitterUrl = twitter !=null ? twitter.Url.Replace("https://x.com/","").Replace("https://twitter.com/","").Replace("https://www.twitter.com/","") : "";
|
var twitterUrl = twitter !=null ? twitter.Url.Replace("https://x.com/","").Replace("https://twitter.com/","").Replace("https://www.twitter.com/","") : "";
|
||||||
var whatsappUrl = whatsapp !=null ? whatsapp.Url.Replace("https://wa.me/","").Replace("whatsapp://","") : "";
|
var whatsappUrl = whatsapp !=null ? whatsapp.Url.Replace("https://wa.me/","").Replace("whatsapp://","") : "";
|
||||||
var instagramUrl = instagram !=null ? instagram.Url.Replace("https://instagram.com/","").Replace("https://www.instagram.com/","") : "";
|
var instagramUrl = instagram !=null ? instagram.Url.Replace("https://instagram.com/","").Replace("https://www.instagram.com/","") : "";
|
||||||
var tiktokUrl = tiktok !=null ? tiktok.Url.Replace("https://tiktok.com/@@","").Replace("https://www.tiktok.com/@@","").Replace("https://vm.tiktok.com/","") : "";
|
var tiktokUrl = tiktok !=null ? tiktok.Url.Replace("https://tiktok.com/@","").Replace("https://www.tiktok.com/@","").Replace("https://vm.tiktok.com/","") : "";
|
||||||
var pinterestUrl = pinterest !=null ? pinterest.Url.Replace("https://pinterest.com/","").Replace("https://www.pinterest.com/","").Replace("https://pin.it/","") : "";
|
var pinterestUrl = pinterest !=null ? pinterest.Url.Replace("https://pinterest.com/","").Replace("https://www.pinterest.com/","").Replace("https://pin.it/","") : "";
|
||||||
var discordUrl = discord !=null ? discord.Url.Replace("https://discord.gg/","").Replace("https://discord.com/invite/","") : "";
|
var discordUrl = discord !=null ? discord.Url.Replace("https://discord.gg/","").Replace("https://discord.com/invite/","") : "";
|
||||||
var kawaiUrl = kawai !=null ? kawai.Url.Replace("https://kawai.com/","").Replace("https://www.kawai.com/","") : "";
|
var kawaiUrl = kawai !=null ? kawai.Url.Replace("https://kawai.com/","").Replace("https://www.kawai.com/","") : "";
|
||||||
}
|
}
|
||||||
<!-- Passo 4: Redes Sociais (Opcional) -->
|
<!-- Passo 5: Redes Sociais (Opcional) -->
|
||||||
<div class="accordion-item">
|
<div class="accordion-item">
|
||||||
<h2 class="accordion-header" id="headingSocial">
|
<h2 class="accordion-header" id="headingSocial">
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseSocial" aria-expanded="false" aria-controls="collapseSocial">
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseSocial" aria-expanded="false" aria-controls="collapseSocial">
|
||||||
<i class="fab fa-twitter me-2"></i>
|
<i class="fab fa-twitter me-2"></i>
|
||||||
Passo 4: Redes Sociais (Opcional)
|
Passo 5: Redes Sociais (Opcional)
|
||||||
<span class="badge bg-success ms-auto me-3" id="step4Status" style="display: none;">✓</span>
|
<span class="badge bg-success ms-auto me-3" id="step5Status" style="display: none;">✓</span>
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="collapseSocial" class="accordion-collapse collapse" aria-labelledby="headingSocial" data-bs-parent="#pageWizard">
|
<div id="collapseSocial" class="accordion-collapse collapse" aria-labelledby="headingSocial" data-bs-parent="#pageWizard">
|
||||||
@ -432,9 +573,12 @@
|
|||||||
<i class="fab fa-whatsapp text-success me-2"></i>
|
<i class="fab fa-whatsapp text-success me-2"></i>
|
||||||
https://wa.me/
|
https://wa.me/
|
||||||
</span>
|
</span>
|
||||||
<input type="text" class="form-control" id="whatsappNumber" placeholder="5511999999999">
|
<input type="text" class="form-control" id="whatsappNumber" placeholder="11987654321">
|
||||||
</div>
|
</div>
|
||||||
<small class="form-text text-muted">Exemplo: 5511999999999 (código do país + DDD + número)</small>
|
<small class="form-text text-muted">
|
||||||
|
<strong>Brasil:</strong> Digite apenas DDD + número (ex: 11987654321). O código 55 será adicionado automaticamente.<br>
|
||||||
|
<strong>Outros países:</strong> Digite o código do país + número completo.
|
||||||
|
</small>
|
||||||
<input asp-for="WhatsAppNumber" type="hidden" value="@(whatsappUrl ?? "")">
|
<input asp-for="WhatsAppNumber" type="hidden" value="@(whatsappUrl ?? "")">
|
||||||
<span asp-validation-for="WhatsAppNumber" class="text-danger"></span>
|
<span asp-validation-for="WhatsAppNumber" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
@ -590,7 +734,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<button type="button" class="btn btn-outline-secondary" onclick="previousStep(3)">
|
<button type="button" class="btn btn-outline-secondary" onclick="previousStep(4)">
|
||||||
<i class="fas fa-arrow-left me-1"></i> Anterior
|
<i class="fas fa-arrow-left me-1"></i> Anterior
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" class="btn btn-success">
|
<button type="submit" class="btn btn-success">
|
||||||
@ -1039,7 +1183,9 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
let linkCount = @Model.Links.Count;
|
let linkCount = @Model.Links.Count;
|
||||||
|
let documentCount = @(Model.Documents?.Count ?? 0);
|
||||||
let currentStep = 1;
|
let currentStep = 1;
|
||||||
|
const totalSteps = 5;
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
// Initialize social media fields
|
// Initialize social media fields
|
||||||
@ -1051,6 +1197,9 @@
|
|||||||
// Initialize URL input handlers
|
// Initialize URL input handlers
|
||||||
initializeUrlInputs();
|
initializeUrlInputs();
|
||||||
|
|
||||||
|
// Initialize document handlers
|
||||||
|
initializeDocumentHandlers();
|
||||||
|
|
||||||
// Check for validation errors and show toast + open accordion
|
// Check for validation errors and show toast + open accordion
|
||||||
checkValidationErrors();
|
checkValidationErrors();
|
||||||
|
|
||||||
@ -1058,14 +1207,22 @@
|
|||||||
checkServerImageErrors();
|
checkServerImageErrors();
|
||||||
|
|
||||||
// Garantir que campos não marcados sejam string vazia ao submeter
|
// Garantir que campos não marcados sejam string vazia ao submeter
|
||||||
$('form').on('submit', function() {
|
$('form').on('submit', function(e) {
|
||||||
ensureUncheckedFieldsAreEmpty();
|
ensureUncheckedFieldsAreEmpty();
|
||||||
|
|
||||||
|
// Validar URLs de redes sociais antes de submeter
|
||||||
|
const validationResult = validateSocialMediaUrls();
|
||||||
|
if (!validationResult.valid) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('Erro de validação:\n\n' + validationResult.errors.join('\n'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Debug: Verificar quais campos de links estão sendo enviados
|
// Debug: Verificar quais campos de links estão sendo enviados
|
||||||
console.log('=== DEBUG FORM SUBMISSION ===');
|
console.log('=== DEBUG FORM SUBMISSION ===');
|
||||||
const formData = new FormData(this);
|
const formData = new FormData(this);
|
||||||
for (let [key, value] of formData.entries()) {
|
for (let [key, value] of formData.entries()) {
|
||||||
if (key.includes('Links[')) {
|
if (key.includes('Links[') || key.includes('Url') || key.includes('Number')) {
|
||||||
console.log(`${key}: ${value}`);
|
console.log(`${key}: ${value}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1097,7 +1254,7 @@
|
|||||||
const maxlinks = @Model.MaxLinksAllowed;
|
const maxlinks = @Model.MaxLinksAllowed;
|
||||||
// Links do modal são limitados pelo MaxLinksAllowed (redes sociais são separadas e opcionais)
|
// Links do modal são limitados pelo MaxLinksAllowed (redes sociais são separadas e opcionais)
|
||||||
if (linkCount >= maxlinks) {
|
if (linkCount >= maxlinks) {
|
||||||
alert('Você atingiu o limite de ' + maxlinks + ' links para seu plano atual. As redes sociais do Passo 4 não contam neste limite.');
|
alert('Você atingiu o limite de ' + maxlinks + ' links para seu plano atual. As redes sociais do Passo 5 não contam neste limite.');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -1189,7 +1346,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getStepName(step) {
|
function getStepName(step) {
|
||||||
const names = ['', 'Basic', 'Theme', 'Links', 'Social'];
|
const names = ['', 'Basic', 'Theme', 'Documents', 'Links', 'Social'];
|
||||||
return names[step];
|
return names[step];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1216,9 +1373,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateProgress() {
|
function updateProgress() {
|
||||||
const progress = (currentStep / 4) * 100;
|
const progress = (currentStep / totalSteps) * 100;
|
||||||
$('.progress-bar').css('width', `${progress}%`).attr('aria-valuenow', progress);
|
$('.progress-bar').css('width', `${progress}%`).attr('aria-valuenow', progress);
|
||||||
$('.progress').next().find('small').first().text(`Passo ${currentStep} de 4`);
|
$('.progress').next().find('small').first().text(`Passo ${currentStep} de ${totalSteps}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateSlug() {
|
function generateSlug() {
|
||||||
@ -1359,6 +1516,167 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initializeDocumentHandlers() {
|
||||||
|
const $documentsContainer = $('#documentsContainer');
|
||||||
|
const $addDocumentBtn = $('#addDocumentBtn');
|
||||||
|
|
||||||
|
if (!$documentsContainer.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleDocumentEmptyState();
|
||||||
|
toggleDocumentAddButton();
|
||||||
|
|
||||||
|
if ($addDocumentBtn.length) {
|
||||||
|
$addDocumentBtn.on('click', function() {
|
||||||
|
addDocumentInput();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).on('click', '.remove-document-btn', function() {
|
||||||
|
const $group = $(this).closest('.document-input-group');
|
||||||
|
const $markRemoval = $group.find('input[name$=".MarkForRemoval"]');
|
||||||
|
const hasExisting = $group.find('input[name$=".DocumentId"]').val();
|
||||||
|
|
||||||
|
if (hasExisting) {
|
||||||
|
$group.addClass('document-removed d-none');
|
||||||
|
if ($markRemoval.length) {
|
||||||
|
$markRemoval.val('true');
|
||||||
|
}
|
||||||
|
$group.find('input[type="file"]').val('');
|
||||||
|
} else {
|
||||||
|
$group.remove();
|
||||||
|
documentCount = Math.max(documentCount - 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDocumentNumbers();
|
||||||
|
toggleDocumentEmptyState();
|
||||||
|
toggleDocumentAddButton();
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('change', '.document-input-group input[type="file"]', function(e) {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.type !== 'application/pdf') {
|
||||||
|
alert('Envie um arquivo em PDF.');
|
||||||
|
$(this).val('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > 10 * 1024 * 1024) {
|
||||||
|
alert('Arquivo muito grande. Tamanho máximo: 10MB.');
|
||||||
|
$(this).val('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleDocumentEmptyState() {
|
||||||
|
const $container = $('#documentsContainer');
|
||||||
|
const $emptyState = $('#documentsEmptyState');
|
||||||
|
if (!$container.length || !$emptyState.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleDocs = $container.find('.document-input-group').not('.document-removed');
|
||||||
|
if (visibleDocs.length === 0) {
|
||||||
|
$emptyState.show();
|
||||||
|
} else {
|
||||||
|
$emptyState.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleDocumentAddButton() {
|
||||||
|
const $button = $('#addDocumentBtn');
|
||||||
|
if (!$button.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxDocs = @Model.MaxDocumentsAllowed;
|
||||||
|
if (maxDocs <= 0) {
|
||||||
|
$button.prop('disabled', false).removeAttr('title');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleDocs = $('#documentsContainer .document-input-group').not('.document-removed').length;
|
||||||
|
if (visibleDocs >= maxDocs) {
|
||||||
|
$button.prop('disabled', true).attr('title', 'Você atingiu o limite de documentos do seu plano.');
|
||||||
|
} else {
|
||||||
|
$button.prop('disabled', false).removeAttr('title');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDocumentInput() {
|
||||||
|
const $container = $('#documentsContainer');
|
||||||
|
if (!$container.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const $button = $('#addDocumentBtn');
|
||||||
|
if ($button.length && $button.prop('disabled')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingIndexes = [];
|
||||||
|
$('input[name^="Documents["]').each(function() {
|
||||||
|
const match = $(this).attr('name').match(/Documents\[(\d+)\]/);
|
||||||
|
if (match) {
|
||||||
|
existingIndexes.push(parseInt(match[1]));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextIndex = existingIndexes.length > 0 ? Math.max(...existingIndexes) + 1 : 0;
|
||||||
|
const displayCount = $container.find('.document-input-group').length + 1;
|
||||||
|
|
||||||
|
const template = `
|
||||||
|
<div class="document-input-group border rounded p-3 mb-3" data-document="${nextIndex}">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-1">Documento ${displayCount}</h6>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger remove-document-btn">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Título <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" name="Documents[${nextIndex}].Title" class="form-control" placeholder="Ex: Apresentação de Serviços">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Descrição (opcional)</label>
|
||||||
|
<textarea name="Documents[${nextIndex}].Description" class="form-control" rows="2" placeholder="Resumo do conteúdo"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Arquivo PDF</label>
|
||||||
|
<input type="file" name="Documents[${nextIndex}].DocumentFile" class="form-control" accept="application/pdf">
|
||||||
|
<small class="form-text text-muted">Envie um arquivo PDF (máx. 10MB).</small>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="Documents[${nextIndex}].Id" value="">
|
||||||
|
<input type="hidden" name="Documents[${nextIndex}].DocumentId" value="">
|
||||||
|
<input type="hidden" name="Documents[${nextIndex}].FileName" value="">
|
||||||
|
<input type="hidden" name="Documents[${nextIndex}].FileSize" value="0">
|
||||||
|
<input type="hidden" name="Documents[${nextIndex}].UploadedAt" value="">
|
||||||
|
<input type="hidden" name="Documents[${nextIndex}].MarkForRemoval" value="false">
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
$container.append(template);
|
||||||
|
documentCount++;
|
||||||
|
$('#documentsEmptyState').hide();
|
||||||
|
toggleDocumentEmptyState();
|
||||||
|
toggleDocumentAddButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDocumentNumbers() {
|
||||||
|
$('#documentsContainer .document-input-group').not('.document-removed').each(function(index) {
|
||||||
|
$(this).attr('data-document', index);
|
||||||
|
$(this).find('h6').text('Documento ' + (index + 1));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function saveNormalLink() {
|
function saveNormalLink() {
|
||||||
const title = $('#linkTitle').val().trim();
|
const title = $('#linkTitle').val().trim();
|
||||||
const url = $('#linkUrl').val().trim();
|
const url = $('#linkUrl').val().trim();
|
||||||
@ -1715,6 +2033,82 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validar URLs de redes sociais antes de submeter o formulário
|
||||||
|
function validateSocialMediaUrls() {
|
||||||
|
const errors = [];
|
||||||
|
const userLang = navigator.language || navigator.userLanguage;
|
||||||
|
const isBrazil = userLang.startsWith('pt-BR') || userLang.startsWith('pt');
|
||||||
|
|
||||||
|
// Validar WhatsApp
|
||||||
|
if ($('#enableWhatsApp').is(':checked')) {
|
||||||
|
const whatsappValue = $('input[name="WhatsAppNumber"]').val().trim();
|
||||||
|
if (whatsappValue && whatsappValue !== ' ') {
|
||||||
|
const cleanNumber = whatsappValue.replace('https://wa.me/', '').replace(/\D/g, '');
|
||||||
|
|
||||||
|
if (isBrazil) {
|
||||||
|
// Brasil: deve ter 13 dígitos (55 + DDD + número)
|
||||||
|
if (cleanNumber.length !== 13 || !cleanNumber.startsWith('55')) {
|
||||||
|
errors.push('⚠️ WhatsApp: Número brasileiro deve ter 13 dígitos (55 + DDD + número).\nExemplo: 5511987654321');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Outros países: validar entre 10 e 15 dígitos
|
||||||
|
if (cleanNumber.length < 10 || cleanNumber.length > 15) {
|
||||||
|
errors.push('⚠️ WhatsApp: Número deve ter entre 10 e 15 dígitos (incluindo código do país).');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar Facebook
|
||||||
|
if ($('#enableFacebook').is(':checked')) {
|
||||||
|
const facebookValue = $('input[name="FacebookUrl"]').val().trim();
|
||||||
|
if (facebookValue && facebookValue !== ' ') {
|
||||||
|
const cleanUrl = facebookValue.replace('https://facebook.com/', '').replace('https://www.facebook.com/', '');
|
||||||
|
if (cleanUrl.length < 3) {
|
||||||
|
errors.push('⚠️ Facebook: Nome de usuário deve ter pelo menos 3 caracteres.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar Instagram
|
||||||
|
if ($('#enableInstagram').is(':checked')) {
|
||||||
|
const instagramValue = $('input[name="InstagramUrl"]').val().trim();
|
||||||
|
if (instagramValue && instagramValue !== ' ') {
|
||||||
|
const cleanUrl = instagramValue.replace('https://instagram.com/', '').replace('https://www.instagram.com/', '');
|
||||||
|
if (cleanUrl.length < 3) {
|
||||||
|
errors.push('⚠️ Instagram: Nome de usuário deve ter pelo menos 3 caracteres.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar Twitter
|
||||||
|
if ($('#enableTwitter').is(':checked')) {
|
||||||
|
const twitterValue = $('input[name="TwitterUrl"]').val().trim();
|
||||||
|
if (twitterValue && twitterValue !== ' ') {
|
||||||
|
const cleanUrl = twitterValue.replace('https://x.com/', '').replace('https://twitter.com/', '');
|
||||||
|
if (cleanUrl.length < 3) {
|
||||||
|
errors.push('⚠️ Twitter/X: Nome de usuário deve ter pelo menos 3 caracteres.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar TikTok
|
||||||
|
if ($('#enableTiktok').is(':checked')) {
|
||||||
|
const tiktokValue = $('input[name="TiktokUrl"]').val().trim();
|
||||||
|
if (tiktokValue && tiktokValue !== ' ') {
|
||||||
|
const cleanUrl = tiktokValue.replace('https://tiktok.com/@@', '').replace('https://www.tiktok.com/@@', '');
|
||||||
|
if (cleanUrl.length < 3) {
|
||||||
|
errors.push('⚠️ TikTok: Nome de usuário deve ter pelo menos 3 caracteres.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: errors.length === 0,
|
||||||
|
errors: errors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Social Media Functions
|
// Social Media Functions
|
||||||
function cleanSocialPrefix(value, socialType) {
|
function cleanSocialPrefix(value, socialType) {
|
||||||
const prefixes = {
|
const prefixes = {
|
||||||
@ -1732,10 +2126,16 @@
|
|||||||
|
|
||||||
for (let prefix of typePrefixes) {
|
for (let prefix of typePrefixes) {
|
||||||
if (value.startsWith(prefix)) {
|
if (value.startsWith(prefix)) {
|
||||||
return value.replace(prefix, '');
|
value = value.replace(prefix, '');
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TikTok: remover @@ do início (o prefixo já tem @@)
|
||||||
|
if (socialType === 'Tiktok' && value.startsWith('@@')) {
|
||||||
|
value = value.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1817,6 +2217,16 @@
|
|||||||
if (isWhatsApp) {
|
if (isWhatsApp) {
|
||||||
// WhatsApp: apenas números
|
// WhatsApp: apenas números
|
||||||
value = value.replace(/\D/g, '');
|
value = value.replace(/\D/g, '');
|
||||||
|
|
||||||
|
// Auto-adicionar código +55 para números brasileiros (11 dígitos sem código país)
|
||||||
|
// Detecta cultura pt-BR para aplicar validação BR
|
||||||
|
const userLang = navigator.language || navigator.userLanguage;
|
||||||
|
const isBrazil = userLang.startsWith('pt-BR') || userLang.startsWith('pt');
|
||||||
|
|
||||||
|
if (isBrazil && value.length === 11 && !value.startsWith('55')) {
|
||||||
|
value = '55' + value;
|
||||||
|
}
|
||||||
|
|
||||||
$(this).val(value);
|
$(this).val(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1839,11 +2249,25 @@
|
|||||||
if (!value) return;
|
if (!value) return;
|
||||||
|
|
||||||
if (isWhatsApp) {
|
if (isWhatsApp) {
|
||||||
// Validar WhatsApp (mínimo 10 dígitos)
|
// Validar WhatsApp com detecção de idioma
|
||||||
if (value.length >= 10) {
|
const userLang = navigator.language || navigator.userLanguage;
|
||||||
input.addClass('is-valid');
|
const isBrazil = userLang.startsWith('pt-BR') || userLang.startsWith('pt');
|
||||||
|
|
||||||
|
if (isBrazil) {
|
||||||
|
// Brasil: validar 13 dígitos (55 + DDD + número)
|
||||||
|
if (value.length === 13 && value.startsWith('55')) {
|
||||||
|
input.addClass('is-valid');
|
||||||
|
} else if (value.length >= 10) {
|
||||||
|
// Ainda está digitando ou formato incorreto
|
||||||
|
input.addClass('is-invalid');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
input.addClass('is-invalid');
|
// Outros países: validar mínimo 10 dígitos (genérico)
|
||||||
|
if (value.length >= 10 && value.length <= 15) {
|
||||||
|
input.addClass('is-valid');
|
||||||
|
} else if (value.length > 0) {
|
||||||
|
input.addClass('is-invalid');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Validar username (mínimo 3 caracteres, sem espaços)
|
// Validar username (mínimo 3 caracteres, sem espaços)
|
||||||
|
|||||||
@ -80,14 +80,14 @@
|
|||||||
<h5 class="mb-0">Básico</h5>
|
<h5 class="mb-0">Básico</h5>
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<div class="pricing-monthly">
|
<div class="pricing-monthly">
|
||||||
<span class="display-5 fw-bold text-primary">R$ 5,90</span>
|
<span class="display-5 fw-bold text-primary">R$ 12,90</span>
|
||||||
<span class="text-muted">/mês</span>
|
<span class="text-muted">/mês</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pricing-yearly d-none">
|
<div class="pricing-yearly d-none">
|
||||||
<span class="display-5 fw-bold text-primary">R$ 59,00</span>
|
<span class="display-5 fw-bold text-primary">R$ 129,00</span>
|
||||||
<span class="text-muted">/ano</span>
|
<span class="text-muted">/ano</span>
|
||||||
<br>
|
<br>
|
||||||
<small class="text-success">Economize R$ 11,80 (2 meses grátis)</small>
|
<small class="text-success">Economize R$ 25,80 (2 meses grátis)</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -154,14 +154,14 @@
|
|||||||
<h5 class="mb-0">Profissional</h5>
|
<h5 class="mb-0">Profissional</h5>
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<div class="pricing-monthly">
|
<div class="pricing-monthly">
|
||||||
<span class="display-5 fw-bold text-warning">R$ 12,90</span>
|
<span class="display-5 fw-bold text-warning">R$ 25,90</span>
|
||||||
<span class="text-muted">/mês</span>
|
<span class="text-muted">/mês</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pricing-yearly d-none">
|
<div class="pricing-yearly d-none">
|
||||||
<span class="display-5 fw-bold text-warning">R$ 129,00</span>
|
<span class="display-5 fw-bold text-warning">R$ 259,00</span>
|
||||||
<span class="text-muted">/ano</span>
|
<span class="text-muted">/ano</span>
|
||||||
<br>
|
<br>
|
||||||
<small class="text-success">Economize R$ 25,80 (2 meses grátis)</small>
|
<small class="text-success">Economize R$ 51,80 (2 meses grátis)</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -231,14 +231,14 @@
|
|||||||
<h5 class="mb-0">Premium</h5>
|
<h5 class="mb-0">Premium</h5>
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<div class="pricing-monthly">
|
<div class="pricing-monthly">
|
||||||
<span class="display-5 fw-bold">R$ 19,90</span>
|
<span class="display-5 fw-bold">R$ 29,90</span>
|
||||||
<span class="opacity-75">/mês</span>
|
<span class="opacity-75">/mês</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pricing-yearly d-none">
|
<div class="pricing-yearly d-none">
|
||||||
<span class="display-5 fw-bold">R$ 199,00</span>
|
<span class="display-5 fw-bold">R$ 299,00</span>
|
||||||
<span class="opacity-75">/ano</span>
|
<span class="opacity-75">/ano</span>
|
||||||
<br>
|
<br>
|
||||||
<small class="text-light">Economize R$ 39,80 (2 meses grátis)</small>
|
<small class="text-light">Economize R$ 59,80 (2 meses grátis)</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<small class="opacity-75">Melhor custo-benefício!</small>
|
<small class="opacity-75">Melhor custo-benefício!</small>
|
||||||
@ -269,6 +269,10 @@
|
|||||||
<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">
|
||||||
|
<i class="text-success me-2">✓</i>
|
||||||
|
Upload de PDFs (até 5 arquivos)
|
||||||
|
</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">Links de produto</span>
|
||||||
@ -312,14 +316,14 @@
|
|||||||
<h5 class="mb-0">Premium + Afiliados</h5>
|
<h5 class="mb-0">Premium + Afiliados</h5>
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<div class="pricing-monthly">
|
<div class="pricing-monthly">
|
||||||
<span class="display-5 fw-bold">R$ 29,90</span>
|
<span class="display-5 fw-bold">R$ 34,90</span>
|
||||||
<span class="opacity-75">/mês</span>
|
<span class="opacity-75">/mês</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pricing-yearly d-none">
|
<div class="pricing-yearly d-none">
|
||||||
<span class="display-5 fw-bold">R$ 299,00</span>
|
<span class="display-5 fw-bold">R$ 349,00</span>
|
||||||
<span class="opacity-75">/ano</span>
|
<span class="opacity-75">/ano</span>
|
||||||
<br>
|
<br>
|
||||||
<small class="text-light">Economize R$ 59,80 (2 meses grátis)</small>
|
<small class="text-light">Economize R$ 69,80 (2 meses grátis)</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<small class="opacity-75">Para monetização!</small>
|
<small class="opacity-75">Para monetização!</small>
|
||||||
@ -354,6 +358,10 @@
|
|||||||
<i class="text-success me-2">✓</i>
|
<i class="text-success me-2">✓</i>
|
||||||
10 links afiliados
|
10 links afiliados
|
||||||
</li>
|
</li>
|
||||||
|
<li class="mb-3">
|
||||||
|
<i class="text-success me-2">✓</i>
|
||||||
|
Upload de PDFs (até 10 arquivos)
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<small class="text-muted">* 20 temas básicos + 20 temas premium exclusivos</small>
|
<small class="text-muted">* 20 temas básicos + 20 temas premium exclusivos</small>
|
||||||
|
|||||||
@ -42,6 +42,14 @@
|
|||||||
|
|
||||||
@await RenderSectionAsync("Styles", required: false)
|
@await RenderSectionAsync("Styles", required: false)
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
(function(c,l,a,r,i,t,y){
|
||||||
|
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
|
||||||
|
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
|
||||||
|
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
|
||||||
|
})(window, document, "clarity", "script", "tzv881489n");
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- Estilos para menu ativo e barra de carregamento -->
|
<!-- Estilos para menu ativo e barra de carregamento -->
|
||||||
<style>
|
<style>
|
||||||
/* Barra de carregamento moderna */
|
/* Barra de carregamento moderna */
|
||||||
@ -158,6 +166,22 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
@* Menu Artigos - Condicional (só aparece se existir ao menos 1 artigo) *@
|
||||||
|
@{
|
||||||
|
var artigosPath = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "Content", "Artigos");
|
||||||
|
var hasArtigos = System.IO.Directory.Exists(artigosPath) &&
|
||||||
|
System.IO.Directory.GetFiles(artigosPath, "*.pt-BR.md").Any();
|
||||||
|
}
|
||||||
|
@if (hasArtigos)
|
||||||
|
{
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link @(ViewBag.IsHomePage == true ? "text-white" : "text-dark")"
|
||||||
|
asp-area="Artigos" asp-controller="Artigos" asp-action="Index">
|
||||||
|
Artigos
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
|
||||||
@* Menu de Moderação via ViewComponent *@
|
@* Menu de Moderação via ViewComponent *@
|
||||||
@await Component.InvokeAsync("ModerationMenu")
|
@await Component.InvokeAsync("ModerationMenu")
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@ -10,6 +10,84 @@
|
|||||||
Layout = isPreview ? "_PreviewLayout" : "_UserPageLayout";
|
Layout = isPreview ? "_PreviewLayout" : "_UserPageLayout";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@functions {
|
||||||
|
/// <summary>
|
||||||
|
/// Normaliza URLs de redes sociais para garantir que sempre tenham protocolo HTTPS
|
||||||
|
/// Corrige URLs que foram salvas sem prefixo HTTP(S)
|
||||||
|
/// </summary>
|
||||||
|
string NormalizeSocialUrl(string url, string icon)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(url)) return "#";
|
||||||
|
|
||||||
|
// Se já tem protocolo, retorna direto
|
||||||
|
if (url.StartsWith("http://") || url.StartsWith("https://"))
|
||||||
|
return url;
|
||||||
|
|
||||||
|
// WhatsApp - garantir prefixo wa.me
|
||||||
|
if (!string.IsNullOrEmpty(icon) && icon.Contains("whatsapp"))
|
||||||
|
{
|
||||||
|
// Remove qualquer prefixo parcial que possa existir
|
||||||
|
var cleanUrl = url.Replace("wa.me/", "").Replace("whatsapp://", "");
|
||||||
|
return $"https://wa.me/{cleanUrl}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Facebook
|
||||||
|
if (!string.IsNullOrEmpty(icon) && icon.Contains("facebook"))
|
||||||
|
{
|
||||||
|
var cleanUrl = url.Replace("facebook.com/", "").Replace("fb.com/", "");
|
||||||
|
return $"https://facebook.com/{cleanUrl}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instagram
|
||||||
|
if (!string.IsNullOrEmpty(icon) && icon.Contains("instagram"))
|
||||||
|
{
|
||||||
|
var cleanUrl = url.Replace("instagram.com/", "").Replace("instagr.am/", "");
|
||||||
|
return $"https://instagram.com/{cleanUrl}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Twitter/X
|
||||||
|
if (!string.IsNullOrEmpty(icon) && (icon.Contains("twitter") || icon.Contains("x-twitter")))
|
||||||
|
{
|
||||||
|
var cleanUrl = url.Replace("x.com/", "").Replace("twitter.com/", "");
|
||||||
|
return $"https://x.com/{cleanUrl}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// TikTok
|
||||||
|
if (!string.IsNullOrEmpty(icon) && icon.Contains("tiktok"))
|
||||||
|
{
|
||||||
|
var cleanUrl = url.Replace("tiktok.com/", "").Replace("tiktok.com/@", "");
|
||||||
|
// Se não tem @, adiciona
|
||||||
|
if (!cleanUrl.StartsWith("@"))
|
||||||
|
cleanUrl = "@" + cleanUrl;
|
||||||
|
return $"https://tiktok.com/{cleanUrl}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pinterest
|
||||||
|
if (!string.IsNullOrEmpty(icon) && icon.Contains("pinterest"))
|
||||||
|
{
|
||||||
|
var cleanUrl = url.Replace("pinterest.com/", "").Replace("pin.it/", "");
|
||||||
|
return $"https://pinterest.com/{cleanUrl}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discord
|
||||||
|
if (!string.IsNullOrEmpty(icon) && icon.Contains("discord"))
|
||||||
|
{
|
||||||
|
var cleanUrl = url.Replace("discord.gg/", "").Replace("discord.com/invite/", "");
|
||||||
|
return $"https://discord.gg/{cleanUrl}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kawai ou qualquer outra rede não identificada
|
||||||
|
// Se contém domínio, apenas adiciona https://
|
||||||
|
if (url.Contains(".") || url.Contains("/"))
|
||||||
|
{
|
||||||
|
return $"https://{url.TrimStart('/')}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: assume que é uma URL completa que está faltando protocolo
|
||||||
|
return $"https://{url}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@section Head {
|
@section Head {
|
||||||
@if (isLivePage)
|
@if (isLivePage)
|
||||||
{
|
{
|
||||||
@ -44,6 +122,25 @@
|
|||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.documents-section .list-group-item {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: inherit;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.documents-section .list-group-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.documents-section .list-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.qrcode-toggle {
|
.qrcode-toggle {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
@ -214,7 +311,7 @@
|
|||||||
(link.Type == BCards.Web.Models.LinkType.Product && !string.IsNullOrEmpty(link.ProductDescription)));
|
(link.Type == BCards.Web.Models.LinkType.Product && !string.IsNullOrEmpty(link.ProductDescription)));
|
||||||
|
|
||||||
<div class="universal-link" data-link-id="@i">
|
<div class="universal-link" data-link-id="@i">
|
||||||
<a href="@link.Url"
|
<a href="@NormalizeSocialUrl(link.Url, link.Icon)"
|
||||||
class="universal-link-header"
|
class="universal-link-header"
|
||||||
onclick="recordClick('@Model.Id', @i)"
|
onclick="recordClick('@Model.Id', @i)"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@ -309,7 +406,61 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
@* Documentos integrados no mesmo container de links *@
|
||||||
|
@if (Model.Documents?.Any() == true)
|
||||||
|
{
|
||||||
|
@for (int docIndex = 0; docIndex < Model.Documents.Count; docIndex++)
|
||||||
|
{
|
||||||
|
var document = Model.Documents[docIndex];
|
||||||
|
var hasDescription = !string.IsNullOrEmpty(document.Description);
|
||||||
|
var uniqueId = $"doc-{docIndex}";
|
||||||
|
|
||||||
|
<div class="universal-link" data-document-id="@uniqueId">
|
||||||
|
<a href="/api/document/@document.FileId"
|
||||||
|
class="universal-link-header"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer">
|
||||||
|
|
||||||
|
<div class="universal-link-content">
|
||||||
|
<div class="link-icon">
|
||||||
|
<i class="fas fa-file-pdf"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="link-text-container">
|
||||||
|
<div class="link-title">@document.Title</div>
|
||||||
|
@if (hasDescription && document.Description.Length > 50)
|
||||||
|
{
|
||||||
|
<div class="link-subtitle">@(document.Description.Substring(0, 50))...</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (hasDescription)
|
||||||
|
{
|
||||||
|
<button class="expand-arrow"
|
||||||
|
type="button"
|
||||||
|
onclick="event.preventDefault(); event.stopPropagation(); toggleLinkDetails('@uniqueId')">
|
||||||
|
<i class="fas fa-chevron-down"></i>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
@if (hasDescription)
|
||||||
|
{
|
||||||
|
<div class="universal-link-details" id="details-@uniqueId">
|
||||||
|
<div class="expanded-description">@document.Description</div>
|
||||||
|
<div class="expanded-action">
|
||||||
|
<i class="fas fa-external-link-alt"></i>
|
||||||
|
Clique no título acima para abrir o PDF
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@if ((Model.Links?.Any(l => l.IsActive) != true) && (Model.Documents?.Any() != true))
|
||||||
{
|
{
|
||||||
<div class="text-muted">
|
<div class="text-muted">
|
||||||
<p>Nenhum link disponível no momento.</p>
|
<p>Nenhum link disponível no momento.</p>
|
||||||
@ -392,7 +543,11 @@
|
|||||||
|
|
||||||
function toggleLinkDetails(linkIndex) {
|
function toggleLinkDetails(linkIndex) {
|
||||||
const currentDetails = document.getElementById('details-' + linkIndex);
|
const currentDetails = document.getElementById('details-' + linkIndex);
|
||||||
const currentArrow = document.querySelector(`[data-link-id="${linkIndex}"] .expand-arrow`);
|
// Suporta tanto data-link-id (links) quanto data-document-id (documentos)
|
||||||
|
let currentArrow = document.querySelector(`[data-link-id="${linkIndex}"] .expand-arrow`);
|
||||||
|
if (!currentArrow) {
|
||||||
|
currentArrow = document.querySelector(`[data-document-id="${linkIndex}"] .expand-arrow`);
|
||||||
|
}
|
||||||
if (!currentDetails || !currentArrow) return;
|
if (!currentDetails || !currentArrow) return;
|
||||||
|
|
||||||
const isCurrentlyExpanded = currentDetails.classList.contains('show');
|
const isCurrentlyExpanded = currentDetails.classList.contains('show');
|
||||||
|
|||||||
@ -22,101 +22,117 @@
|
|||||||
"Basic": {
|
"Basic": {
|
||||||
"Name": "Básico",
|
"Name": "Básico",
|
||||||
"PriceId": "price_1RycPaBMIadsOxJVKioZZofK",
|
"PriceId": "price_1RycPaBMIadsOxJVKioZZofK",
|
||||||
"Price": 5.90,
|
"Price": 12.90,
|
||||||
"MaxPages": 3,
|
"MaxPages": 3,
|
||||||
"MaxLinks": 8,
|
"MaxLinks": 8,
|
||||||
"AllowPremiumThemes": false,
|
"AllowPremiumThemes": false,
|
||||||
"AllowProductLinks": false,
|
"AllowProductLinks": false,
|
||||||
"AllowAnalytics": true,
|
"AllowAnalytics": true,
|
||||||
|
"AllowDocumentUpload": false,
|
||||||
|
"MaxDocuments": 0,
|
||||||
"Features": [ "URL Personalizada", "20 temas básicos", "Analytics simples" ],
|
"Features": [ "URL Personalizada", "20 temas básicos", "Analytics simples" ],
|
||||||
"Interval": "month"
|
"Interval": "month"
|
||||||
},
|
},
|
||||||
"Professional": {
|
"Professional": {
|
||||||
"Name": "Profissional",
|
"Name": "Profissional",
|
||||||
"PriceId": "price_1RycQmBMIadsOxJVGqjVMaOj",
|
"PriceId": "price_1RycQmBMIadsOxJVGqjVMaOj",
|
||||||
"Price": 12.90,
|
"Price": 25.90,
|
||||||
"MaxPages": 5,
|
"MaxPages": 5,
|
||||||
"MaxLinks": 20,
|
"MaxLinks": 20,
|
||||||
"AllowPremiumThemes": false,
|
"AllowPremiumThemes": false,
|
||||||
"AllowProductLinks": false,
|
"AllowProductLinks": false,
|
||||||
"AllowAnalytics": true,
|
"AllowAnalytics": true,
|
||||||
|
"AllowDocumentUpload": false,
|
||||||
|
"MaxDocuments": 0,
|
||||||
"Features": [ "URL Personalizada", "20 temas básicos", "Analytics avançado" ],
|
"Features": [ "URL Personalizada", "20 temas básicos", "Analytics avançado" ],
|
||||||
"Interval": "month"
|
"Interval": "month"
|
||||||
},
|
},
|
||||||
"Premium": {
|
"Premium": {
|
||||||
"Name": "Premium",
|
"Name": "Premium",
|
||||||
"PriceId": "price_1RycRUBMIadsOxJVkxGOh3uu",
|
"PriceId": "price_1RycRUBMIadsOxJVkxGOh3uu",
|
||||||
"Price": 19.90,
|
"Price": 29.90,
|
||||||
"MaxPages": 15,
|
"MaxPages": 15,
|
||||||
"MaxLinks": -1,
|
"MaxLinks": -1,
|
||||||
"AllowPremiumThemes": true,
|
"AllowPremiumThemes": true,
|
||||||
"AllowProductLinks": false,
|
"AllowProductLinks": false,
|
||||||
"AllowAnalytics": true,
|
"AllowAnalytics": true,
|
||||||
"SpecialModeration": false,
|
"SpecialModeration": false,
|
||||||
"Features": [ "URL Personalizada", "40 temas (básicos + premium)", "Suporte prioritário", "Links ilimitados" ],
|
"AllowDocumentUpload": true,
|
||||||
|
"MaxDocuments": 5,
|
||||||
|
"Features": [ "URL Personalizada", "40 temas (básicos + premium)", "Suporte prioritário", "Links ilimitados", "Upload de PDFs (até 5 arquivos)" ],
|
||||||
"Interval": "month"
|
"Interval": "month"
|
||||||
},
|
},
|
||||||
"PremiumAffiliate": {
|
"PremiumAffiliate": {
|
||||||
"Name": "Premium+Afiliados",
|
"Name": "Premium+Afiliados",
|
||||||
"PriceId": "price_1RycTaBMIadsOxJVeDLseXQq",
|
"PriceId": "price_1RycTaBMIadsOxJVeDLseXQq",
|
||||||
"Price": 29.90,
|
"Price": 34.90,
|
||||||
"MaxPages": 15,
|
"MaxPages": 15,
|
||||||
"MaxLinks": -1,
|
"MaxLinks": -1,
|
||||||
"AllowPremiumThemes": true,
|
"AllowPremiumThemes": true,
|
||||||
"AllowProductLinks": true,
|
"AllowProductLinks": true,
|
||||||
"AllowAnalytics": true,
|
"AllowAnalytics": true,
|
||||||
"SpecialModeration": true,
|
"SpecialModeration": true,
|
||||||
"Features": [ "Tudo do Premium", "Links de produto", "Moderação especial", "Até 10 links afiliados" ],
|
"AllowDocumentUpload": true,
|
||||||
|
"MaxDocuments": 10,
|
||||||
|
"Features": [ "Tudo do Premium", "Links de produto", "Moderação especial", "Até 10 links afiliados", "Upload de PDFs (até 10 arquivos)" ],
|
||||||
"Interval": "month"
|
"Interval": "month"
|
||||||
},
|
},
|
||||||
"BasicYearly": {
|
"BasicYearly": {
|
||||||
"Name": "Básico Anual",
|
"Name": "Básico Anual",
|
||||||
"PriceId": "price_1RycWgBMIadsOxJVGdtEeoMS",
|
"PriceId": "price_1RycWgBMIadsOxJVGdtEeoMS",
|
||||||
"Price": 59.00,
|
"Price": 129.00,
|
||||||
"MaxPages": 3,
|
"MaxPages": 3,
|
||||||
"MaxLinks": 8,
|
"MaxLinks": 8,
|
||||||
"AllowPremiumThemes": false,
|
"AllowPremiumThemes": false,
|
||||||
"AllowProductLinks": false,
|
"AllowProductLinks": false,
|
||||||
"AllowAnalytics": true,
|
"AllowAnalytics": true,
|
||||||
|
"AllowDocumentUpload": false,
|
||||||
|
"MaxDocuments": 0,
|
||||||
"Features": [ "URL Personalizada", "20 temas básicos", "Analytics simples", "Economize R$ 11,80 (2 meses grátis)" ],
|
"Features": [ "URL Personalizada", "20 temas básicos", "Analytics simples", "Economize R$ 11,80 (2 meses grátis)" ],
|
||||||
"Interval": "year"
|
"Interval": "year"
|
||||||
},
|
},
|
||||||
"ProfessionalYearly": {
|
"ProfessionalYearly": {
|
||||||
"Name": "Profissional Anual",
|
"Name": "Profissional Anual",
|
||||||
"PriceId": "price_1RycXdBMIadsOxJV5cNX7dHm",
|
"PriceId": "price_1RycXdBMIadsOxJV5cNX7dHm",
|
||||||
"Price": 129.00,
|
"Price": 259.00,
|
||||||
"MaxPages": 5,
|
"MaxPages": 5,
|
||||||
"MaxLinks": 20,
|
"MaxLinks": 20,
|
||||||
"AllowPremiumThemes": false,
|
"AllowPremiumThemes": false,
|
||||||
"AllowProductLinks": false,
|
"AllowProductLinks": false,
|
||||||
"AllowAnalytics": true,
|
"AllowAnalytics": true,
|
||||||
|
"AllowDocumentUpload": false,
|
||||||
|
"MaxDocuments": 0,
|
||||||
"Features": [ "URL Personalizada", "20 temas básicos", "Analytics avançado", "Economize R$ 25,80 (2 meses grátis)" ],
|
"Features": [ "URL Personalizada", "20 temas básicos", "Analytics avançado", "Economize R$ 25,80 (2 meses grátis)" ],
|
||||||
"Interval": "year"
|
"Interval": "year"
|
||||||
},
|
},
|
||||||
"PremiumYearly": {
|
"PremiumYearly": {
|
||||||
"Name": "Premium Anual",
|
"Name": "Premium Anual",
|
||||||
"PriceId": "price_1RycYnBMIadsOxJVPdKmzy4m",
|
"PriceId": "price_1RycYnBMIadsOxJVPdKmzy4m",
|
||||||
"Price": 199.00,
|
"Price": 299.00,
|
||||||
"MaxPages": 15,
|
"MaxPages": 15,
|
||||||
"MaxLinks": -1,
|
"MaxLinks": -1,
|
||||||
"AllowPremiumThemes": true,
|
"AllowPremiumThemes": true,
|
||||||
"AllowProductLinks": false,
|
"AllowProductLinks": false,
|
||||||
"AllowAnalytics": true,
|
"AllowAnalytics": true,
|
||||||
"SpecialModeration": false,
|
"SpecialModeration": false,
|
||||||
"Features": [ "URL Personalizada", "40 temas (básicos + premium)", "Suporte prioritário", "Links ilimitados", "Economize R$ 39,80 (2 meses grátis)" ],
|
"AllowDocumentUpload": true,
|
||||||
|
"MaxDocuments": 5,
|
||||||
|
"Features": [ "URL Personalizada", "40 temas (básicos + premium)", "Suporte prioritário", "Links ilimitados", "Upload de PDFs (até 5 arquivos)", "Economize R$ 39,80 (2 meses grátis)" ],
|
||||||
"Interval": "year"
|
"Interval": "year"
|
||||||
},
|
},
|
||||||
"PremiumAffiliateYearly": {
|
"PremiumAffiliateYearly": {
|
||||||
"Name": "Premium+Afiliados Anual",
|
"Name": "Premium+Afiliados Anual",
|
||||||
"PriceId": "price_1RycaEBMIadsOxJVEhsdB2Y1",
|
"PriceId": "price_1RycaEBMIadsOxJVEhsdB2Y1",
|
||||||
"Price": 299.00,
|
"Price": 349.00,
|
||||||
"MaxPages": 15,
|
"MaxPages": 15,
|
||||||
"MaxLinks": -1,
|
"MaxLinks": -1,
|
||||||
"AllowPremiumThemes": true,
|
"AllowPremiumThemes": true,
|
||||||
"AllowProductLinks": true,
|
"AllowProductLinks": true,
|
||||||
"AllowAnalytics": true,
|
"AllowAnalytics": true,
|
||||||
"SpecialModeration": 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)" ],
|
"AllowDocumentUpload": true,
|
||||||
|
"MaxDocuments": 10,
|
||||||
|
"Features": [ "Tudo do Premium", "Links de produto", "Moderação especial", "Até 10 links afiliados", "Upload de PDFs (até 10 arquivos)", "Economize R$ 59,80 (2 meses grátis)" ],
|
||||||
"Interval": "year"
|
"Interval": "year"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user