feat/live-preview #1
228
CLAUDE.md
Normal file
228
CLAUDE.md
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
BCards is a professional LinkTree clone built in ASP.NET Core MVC, targeting Brazilian and Spanish markets. It provides hierarchical URLs organized by business categories (/page/{category}/{slug}), integrated Stripe payments, and a sophisticated moderation system with preview tokens.
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
### Build & Run
|
||||||
|
```bash
|
||||||
|
# Restore dependencies
|
||||||
|
dotnet restore
|
||||||
|
|
||||||
|
# Build solution
|
||||||
|
dotnet build
|
||||||
|
|
||||||
|
# Run development server
|
||||||
|
cd src/BCards.Web
|
||||||
|
dotnet run
|
||||||
|
# Access: https://localhost:49178
|
||||||
|
|
||||||
|
# Run with Docker
|
||||||
|
docker-compose up -d
|
||||||
|
# Access: http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
dotnet test
|
||||||
|
|
||||||
|
# Run tests with coverage
|
||||||
|
dotnet test --collect:"XPlat Code Coverage"
|
||||||
|
|
||||||
|
# Run specific test
|
||||||
|
dotnet test --filter "TestClassName"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
### Technology Stack
|
||||||
|
- **Framework**: ASP.NET Core MVC (.NET 8)
|
||||||
|
- **Database**: MongoDB 7.0 with MongoDB.Driver 2.25.0
|
||||||
|
- **Authentication**: OAuth 2.0 (Google + Microsoft)
|
||||||
|
- **Payments**: Stripe.NET 44.7.0
|
||||||
|
- **Frontend**: Bootstrap 5.3.2, jQuery 3.7.1, vanilla JavaScript
|
||||||
|
- **Email**: SendGrid 9.29.3
|
||||||
|
- **Containerization**: Docker + Docker Compose
|
||||||
|
|
||||||
|
### Core Architecture Patterns
|
||||||
|
- **MVC Pattern**: Controllers handle HTTP requests, Views render UI, Models represent data
|
||||||
|
- **Repository Pattern**: Data access abstraction via IUserPageRepository, ICategoryRepository, etc.
|
||||||
|
- **Service Layer**: Business logic encapsulation (IUserPageService, IModerationService, etc.)
|
||||||
|
- **Dependency Injection**: Built-in ASP.NET Core DI container
|
||||||
|
- **Domain-Driven Design**: Rich domain models with business logic
|
||||||
|
|
||||||
|
### Key Business Logic
|
||||||
|
|
||||||
|
#### Page Status System
|
||||||
|
Pages follow this lifecycle with explicit numeric enum values:
|
||||||
|
- `Creating = 6`: Development phase, requires preview tokens
|
||||||
|
- `PendingModeration = 4`: Submitted for approval, requires preview tokens
|
||||||
|
- `Rejected = 5`: Failed moderation, requires preview tokens
|
||||||
|
- `Active = 0`: Live and publicly accessible
|
||||||
|
- `Inactive = 3`: Paused by user
|
||||||
|
- `Expired = 1`: Trial expired, redirects to pricing
|
||||||
|
- `PendingPayment = 2`: Payment overdue, shows warning
|
||||||
|
|
||||||
|
#### Moderation System
|
||||||
|
- Content approval workflow with attempts tracking
|
||||||
|
- Preview tokens with 4-hour expiration for non-Active pages
|
||||||
|
- Email notifications for status changes
|
||||||
|
- Automatic status transitions based on user actions
|
||||||
|
|
||||||
|
#### Pricing Strategy
|
||||||
|
Three-tier system with psychological pricing (decoy effect):
|
||||||
|
- Basic (R$ 9.90/mês): 5 links, basic themes
|
||||||
|
- Professional (R$ 24.90/mês): 15 links, all themes - *DECOY*
|
||||||
|
- Premium (R$ 29.90/mês): Unlimited links, custom themes
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
```
|
||||||
|
src/BCards.Web/
|
||||||
|
├── Controllers/ # MVC Controllers (9 total)
|
||||||
|
│ ├── AdminController # User dashboard and page management
|
||||||
|
│ ├── AuthController # OAuth authentication
|
||||||
|
│ ├── HomeController # Public pages and landing
|
||||||
|
│ ├── PaymentController # Stripe integration
|
||||||
|
│ ├── ModerationController # Content approval system
|
||||||
|
│ └── UserPageController # Public page display
|
||||||
|
├── Models/ # Domain entities (12 total)
|
||||||
|
│ ├── User # Authentication and subscriptions
|
||||||
|
│ ├── UserPage # Main business card entity
|
||||||
|
│ ├── LinkItem # Individual links with analytics
|
||||||
|
│ └── Category # Business categories
|
||||||
|
├── Services/ # Business logic (20 services)
|
||||||
|
│ ├── IUserPageService # Core page operations
|
||||||
|
│ ├── IModerationService # Content approval
|
||||||
|
│ ├── IAuthService # Authentication
|
||||||
|
│ └── IPaymentService # Stripe integration
|
||||||
|
├── Repositories/ # Data access (8 repositories)
|
||||||
|
├── ViewModels/ # View-specific models
|
||||||
|
├── Middleware/ # Custom middleware (4 pieces)
|
||||||
|
│ ├── PageStatusMiddleware # Handles page access by status
|
||||||
|
│ ├── ModerationAuthMiddleware # Admin access control
|
||||||
|
│ └── PreviewTokenMiddleware # Preview token validation
|
||||||
|
└── Views/ # Razor templates with Bootstrap 5
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Design (MongoDB)
|
||||||
|
|
||||||
|
#### Core Collections
|
||||||
|
- `users`: Authentication, subscription status, Stripe customer data
|
||||||
|
- `userpages`: Business cards with status, links, themes, moderation history
|
||||||
|
- `categories`: Business categories with SEO metadata
|
||||||
|
- `subscriptions`: Stripe subscription tracking
|
||||||
|
|
||||||
|
#### Important Indexes
|
||||||
|
- Compound: `{category: 1, slug: 1}` for page lookups
|
||||||
|
- User pages: `{userId: 1, status: 1}` for dashboard filtering
|
||||||
|
- Active pages: `{status: 1, category: 1}` for public listings
|
||||||
|
|
||||||
|
### Key Features Implementation
|
||||||
|
|
||||||
|
#### Preview Token System
|
||||||
|
Non-Active pages require preview tokens for access:
|
||||||
|
```csharp
|
||||||
|
// Generate fresh token (4-hour expiry)
|
||||||
|
POST /Admin/GeneratePreviewToken/{id}
|
||||||
|
|
||||||
|
// Access page with token
|
||||||
|
GET /page/{category}/{slug}?preview={token}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### OAuth Integration
|
||||||
|
Supports Google and Microsoft OAuth with automatic user creation and session management.
|
||||||
|
|
||||||
|
#### Stripe Payment Flow
|
||||||
|
Complete subscription lifecycle:
|
||||||
|
1. Checkout session creation
|
||||||
|
2. Webhook handling for events
|
||||||
|
3. Subscription status updates
|
||||||
|
4. Plan limitation enforcement
|
||||||
|
|
||||||
|
#### Dynamic Theming
|
||||||
|
CSS generation system with customizable colors, backgrounds, and layouts based on user's plan limitations.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Required Environment Variables
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"MongoDb": {
|
||||||
|
"ConnectionString": "mongodb://localhost:27017",
|
||||||
|
"DatabaseName": "BCardsDB"
|
||||||
|
},
|
||||||
|
"Stripe": {
|
||||||
|
"PublishableKey": "pk_test_...",
|
||||||
|
"SecretKey": "sk_test_...",
|
||||||
|
"WebhookSecret": "whsec_..."
|
||||||
|
},
|
||||||
|
"Authentication": {
|
||||||
|
"Google": { "ClientId": "...", "ClientSecret": "..." },
|
||||||
|
"Microsoft": { "ClientId": "...", "ClientSecret": "..." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Setup
|
||||||
|
1. Install .NET 8 SDK, MongoDB 4.4+
|
||||||
|
2. Configure OAuth credentials (Google Cloud Console, Azure Portal)
|
||||||
|
3. Set up Stripe account with test keys
|
||||||
|
4. Configure webhook endpoints for `/webhook/stripe`
|
||||||
|
|
||||||
|
## Important Implementation Notes
|
||||||
|
|
||||||
|
### Page Status Middleware
|
||||||
|
`PageStatusMiddleware` intercepts all `/page/{category}/{slug}` requests and enforces access rules based on page status. Non-Active pages require valid preview tokens.
|
||||||
|
|
||||||
|
### Moderation Workflow
|
||||||
|
1. Pages start as `Creating` status
|
||||||
|
2. Users click "Submit for Moderation" → `PendingModeration`
|
||||||
|
3. Moderators approve/reject → `Active` or `Rejected`
|
||||||
|
4. Rejected pages can be edited and resubmitted
|
||||||
|
|
||||||
|
### Preview Token Security
|
||||||
|
- Tokens expire after 4 hours
|
||||||
|
- Generated on-demand via AJAX calls
|
||||||
|
- Required for Creating, PendingModeration, and Rejected pages
|
||||||
|
- Validated by middleware before page access
|
||||||
|
|
||||||
|
### Plan Limitations
|
||||||
|
Enforced throughout the application:
|
||||||
|
- Link count limits per plan
|
||||||
|
- Theme availability restrictions
|
||||||
|
- Analytics access control
|
||||||
|
- Page creation limits
|
||||||
|
|
||||||
|
## Common Development Patterns
|
||||||
|
|
||||||
|
### Repository Usage
|
||||||
|
```csharp
|
||||||
|
var page = await _userPageService.GetPageByIdAsync(id);
|
||||||
|
await _userPageService.UpdatePageAsync(page);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Layer Pattern
|
||||||
|
Business logic resides in services, not controllers:
|
||||||
|
```csharp
|
||||||
|
public class UserPageService : IUserPageService
|
||||||
|
{
|
||||||
|
private readonly IUserPageRepository _repository;
|
||||||
|
// Implementation with business rules
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Status-Based Logic
|
||||||
|
Always check page status before operations:
|
||||||
|
```csharp
|
||||||
|
if (page.Status == PageStatus.Creating || page.Status == PageStatus.Rejected)
|
||||||
|
{
|
||||||
|
// Allow editing
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This architecture supports a production-ready SaaS application with complex business rules, payment integration, and content moderation workflows.
|
||||||
@ -43,6 +43,8 @@ public class AdminController : Controller
|
|||||||
[Route("Dashboard")]
|
[Route("Dashboard")]
|
||||||
public async Task<IActionResult> Dashboard()
|
public async Task<IActionResult> Dashboard()
|
||||||
{
|
{
|
||||||
|
ViewBag.IsHomePage = false; // Menu normal do dashboard
|
||||||
|
|
||||||
var user = await _authService.GetCurrentUserAsync(User);
|
var user = await _authService.GetCurrentUserAsync(User);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
return RedirectToAction("Login", "Auth");
|
return RedirectToAction("Login", "Auth");
|
||||||
@ -100,6 +102,8 @@ public class AdminController : Controller
|
|||||||
[Route("ManagePage")]
|
[Route("ManagePage")]
|
||||||
public async Task<IActionResult> ManagePage(string id = null)
|
public async Task<IActionResult> ManagePage(string id = null)
|
||||||
{
|
{
|
||||||
|
ViewBag.IsHomePage = false;
|
||||||
|
|
||||||
var user = await _authService.GetCurrentUserAsync(User);
|
var user = await _authService.GetCurrentUserAsync(User);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
return RedirectToAction("Login", "Auth");
|
return RedirectToAction("Login", "Auth");
|
||||||
@ -146,6 +150,8 @@ public class AdminController : Controller
|
|||||||
[Route("ManagePage")]
|
[Route("ManagePage")]
|
||||||
public async Task<IActionResult> ManagePage(ManagePageViewModel model)
|
public async Task<IActionResult> ManagePage(ManagePageViewModel model)
|
||||||
{
|
{
|
||||||
|
ViewBag.IsHomePage = false;
|
||||||
|
|
||||||
var user = await _authService.GetCurrentUserAsync(User);
|
var user = await _authService.GetCurrentUserAsync(User);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
return RedirectToAction("Login", "Auth");
|
return RedirectToAction("Login", "Auth");
|
||||||
@ -199,29 +205,20 @@ public class AdminController : Controller
|
|||||||
var userPage = await MapToUserPage(model, user.Id);
|
var userPage = await MapToUserPage(model, user.Id);
|
||||||
_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 PendingModeration for new pages
|
// Set status to Creating for new pages
|
||||||
userPage.Status = ViewModels.PageStatus.PendingModeration;
|
userPage.Status = ViewModels.PageStatus.Creating;
|
||||||
|
|
||||||
await _userPageService.CreatePageAsync(userPage);
|
await _userPageService.CreatePageAsync(userPage);
|
||||||
_logger.LogInformation("Page created successfully!");
|
_logger.LogInformation("Page created successfully!");
|
||||||
|
|
||||||
// Generate preview token and send for moderation
|
// Generate preview token for development
|
||||||
var previewToken = await _moderationService.GeneratePreviewTokenAsync(userPage.Id);
|
var previewToken = await _moderationService.GeneratePreviewTokenAsync(userPage.Id);
|
||||||
var previewUrl = $"{Request.Scheme}://{Request.Host}/page/{userPage.Category}/{userPage.Slug}?preview={previewToken}";
|
var previewUrl = $"{Request.Scheme}://{Request.Host}/page/{userPage.Category}/{userPage.Slug}?preview={previewToken}";
|
||||||
userPage.PreviewToken = previewToken;
|
userPage.PreviewToken = previewToken;
|
||||||
userPage.PreviewTokenExpiry = DateTime.UtcNow.AddHours(4);
|
userPage.PreviewTokenExpiry = DateTime.UtcNow.AddHours(4);
|
||||||
await _userPageService.UpdatePageAsync(userPage);
|
await _userPageService.UpdatePageAsync(userPage);
|
||||||
|
|
||||||
// Send email to user
|
TempData["Success"] = "Página criada com sucesso! Use o botão 'Enviar para Moderação' quando estiver pronta.";
|
||||||
await _emailService.SendModerationStatusAsync(
|
|
||||||
user.Email,
|
|
||||||
user.Name,
|
|
||||||
userPage.DisplayName,
|
|
||||||
"pending",
|
|
||||||
null,
|
|
||||||
previewUrl);
|
|
||||||
|
|
||||||
TempData["Success"] = "Página enviada para moderação! Você receberá um email quando for aprovada.";
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -506,6 +503,8 @@ public class AdminController : Controller
|
|||||||
[Route("Analytics")]
|
[Route("Analytics")]
|
||||||
public async Task<IActionResult> Analytics()
|
public async Task<IActionResult> Analytics()
|
||||||
{
|
{
|
||||||
|
ViewBag.IsHomePage = false;
|
||||||
|
|
||||||
var user = await _authService.GetCurrentUserAsync(User);
|
var user = await _authService.GetCurrentUserAsync(User);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
return RedirectToAction("Login", "Auth");
|
return RedirectToAction("Login", "Auth");
|
||||||
@ -753,4 +752,136 @@ public class AdminController : Controller
|
|||||||
|
|
||||||
page.Links.AddRange(socialLinks);
|
page.Links.AddRange(socialLinks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("SubmitForModeration/{id}")]
|
||||||
|
public async Task<IActionResult> SubmitForModeration(string id)
|
||||||
|
{
|
||||||
|
var user = await _authService.GetCurrentUserAsync(User);
|
||||||
|
if (user == null)
|
||||||
|
return Json(new { success = false, message = "Usuário não autenticado" });
|
||||||
|
|
||||||
|
var pageItem = await _userPageService.GetPageByIdAsync(id);
|
||||||
|
if (pageItem == null || pageItem.UserId != user.Id)
|
||||||
|
return Json(new { success = false, message = "Página não encontrada" });
|
||||||
|
|
||||||
|
// Validar status atual
|
||||||
|
if (pageItem.Status != ViewModels.PageStatus.Creating && pageItem.Status != ViewModels.PageStatus.Rejected)
|
||||||
|
return Json(new { success = false, message = "Página não pode ser enviada para moderação neste momento" });
|
||||||
|
|
||||||
|
// Validar se tem pelo menos 1 link ativo
|
||||||
|
var activeLinksCount = pageItem.Links?.Count(l => l.IsActive) ?? 0;
|
||||||
|
if (activeLinksCount < 1)
|
||||||
|
return Json(new { success = false, message = "Página deve ter pelo menos 1 link ativo para ser enviada" });
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Mudar status para PendingModeration
|
||||||
|
pageItem.Status = ViewModels.PageStatus.PendingModeration;
|
||||||
|
pageItem.ModerationAttempts++;
|
||||||
|
pageItem.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await _userPageService.UpdatePageAsync(pageItem);
|
||||||
|
|
||||||
|
// Enviar email de notificação ao usuário
|
||||||
|
await _emailService.SendModerationStatusAsync(
|
||||||
|
user.Email,
|
||||||
|
user.Name,
|
||||||
|
pageItem.DisplayName,
|
||||||
|
"pending",
|
||||||
|
null,
|
||||||
|
$"{Request.Scheme}://{Request.Host}/page/{pageItem.Category}/{pageItem.Slug}?preview={pageItem.PreviewToken}");
|
||||||
|
|
||||||
|
_logger.LogInformation($"Page {pageItem.Id} submitted for moderation by user {user.Id}");
|
||||||
|
|
||||||
|
return Json(new {
|
||||||
|
success = true,
|
||||||
|
message = "Página enviada para moderação com sucesso! Você receberá um email quando for processada.",
|
||||||
|
newStatus = "PendingModeration"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error submitting page {id} for moderation");
|
||||||
|
return Json(new { success = false, message = "Erro interno. Tente novamente." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("RefreshPreviewToken/{id}")]
|
||||||
|
public async Task<IActionResult> RefreshPreviewToken(string id)
|
||||||
|
{
|
||||||
|
var user = await _authService.GetCurrentUserAsync(User);
|
||||||
|
if (user == null)
|
||||||
|
return Json(new { success = false, message = "Não autorizado" });
|
||||||
|
|
||||||
|
var pageItem = await _userPageService.GetPageByIdAsync(id);
|
||||||
|
if (pageItem == null || pageItem.UserId != user.Id)
|
||||||
|
return Json(new { success = false, message = "Página não encontrada" });
|
||||||
|
|
||||||
|
// Só renovar token para páginas "Creating"
|
||||||
|
if (pageItem.Status != ViewModels.PageStatus.Creating)
|
||||||
|
return Json(new { success = false, message = "Token só pode ser renovado para páginas em desenvolvimento" });
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Gerar novo token com 4 horas de validade
|
||||||
|
var newToken = await _moderationService.GeneratePreviewTokenAsync(pageItem.Id);
|
||||||
|
|
||||||
|
var previewUrl = $"{Request.Scheme}://{Request.Host}/page/{pageItem.Category}/{pageItem.Slug}?preview={newToken}";
|
||||||
|
|
||||||
|
return Json(new {
|
||||||
|
success = true,
|
||||||
|
previewToken = newToken,
|
||||||
|
previewUrl = previewUrl,
|
||||||
|
expiresAt = DateTime.UtcNow.AddHours(4).ToString("yyyy-MM-dd HH:mm:ss")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error refreshing preview token for page {id}");
|
||||||
|
return Json(new { success = false, message = "Erro ao renovar token" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("GeneratePreviewToken/{id}")]
|
||||||
|
public async Task<IActionResult> GeneratePreviewToken(string id)
|
||||||
|
{
|
||||||
|
var user = await _authService.GetCurrentUserAsync(User);
|
||||||
|
if (user == null)
|
||||||
|
return Json(new { success = false, message = "Usuário não autenticado" });
|
||||||
|
|
||||||
|
var pageItem = await _userPageService.GetPageByIdAsync(id);
|
||||||
|
if (pageItem == null || pageItem.UserId != user.Id)
|
||||||
|
return Json(new { success = false, message = "Página não encontrada" });
|
||||||
|
|
||||||
|
// Verificar se página pode ter preview
|
||||||
|
if (pageItem.Status != ViewModels.PageStatus.Creating &&
|
||||||
|
pageItem.Status != ViewModels.PageStatus.PendingModeration &&
|
||||||
|
pageItem.Status != ViewModels.PageStatus.Rejected)
|
||||||
|
{
|
||||||
|
return Json(new { success = false, message = "Preview não disponível para este status" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Gerar novo token com 4 horas de validade
|
||||||
|
var newToken = await _moderationService.GeneratePreviewTokenAsync(pageItem.Id);
|
||||||
|
|
||||||
|
_logger.LogInformation($"Preview token generated for page {pageItem.Id} by user {user.Id}");
|
||||||
|
|
||||||
|
return Json(new {
|
||||||
|
success = true,
|
||||||
|
previewToken = newToken,
|
||||||
|
message = "Preview gerado com sucesso!",
|
||||||
|
expiresAt = DateTime.UtcNow.AddHours(4).ToString("yyyy-MM-dd HH:mm:ss")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error generating preview token for page {id}");
|
||||||
|
return Json(new { success = false, message = "Erro interno. Tente novamente." });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -16,6 +16,7 @@ public class HomeController : Controller
|
|||||||
|
|
||||||
public async Task<IActionResult> Index()
|
public async Task<IActionResult> Index()
|
||||||
{
|
{
|
||||||
|
ViewBag.IsHomePage = true; // Flag para identificar home
|
||||||
ViewBag.Categories = await _categoryService.GetAllCategoriesAsync();
|
ViewBag.Categories = await _categoryService.GetAllCategoriesAsync();
|
||||||
ViewBag.RecentPages = await _userPageService.GetRecentPagesAsync(6);
|
ViewBag.RecentPages = await _userPageService.GetRecentPagesAsync(6);
|
||||||
return View();
|
return View();
|
||||||
@ -24,18 +25,21 @@ public class HomeController : Controller
|
|||||||
[Route("Privacy")]
|
[Route("Privacy")]
|
||||||
public IActionResult Privacy()
|
public IActionResult Privacy()
|
||||||
{
|
{
|
||||||
|
ViewBag.IsHomePage = true;
|
||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Route("Pricing")]
|
[Route("Pricing")]
|
||||||
public IActionResult Pricing()
|
public IActionResult Pricing()
|
||||||
{
|
{
|
||||||
|
ViewBag.IsHomePage = true;
|
||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Route("categoria/{categorySlug}")]
|
[Route("categoria/{categorySlug}")]
|
||||||
public async Task<IActionResult> Category(string categorySlug)
|
public async Task<IActionResult> Category(string categorySlug)
|
||||||
{
|
{
|
||||||
|
ViewBag.IsHomePage = true;
|
||||||
var category = await _categoryService.GetCategoryBySlugAsync(categorySlug);
|
var category = await _categoryService.GetCategoryBySlugAsync(categorySlug);
|
||||||
if (category == null)
|
if (category == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|||||||
@ -51,9 +51,33 @@ public class PageStatusMiddleware
|
|||||||
await context.Response.WriteAsync("Página temporariamente indisponível.");
|
await context.Response.WriteAsync("Página temporariamente indisponível.");
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
case PageStatus.Creating:
|
||||||
|
case PageStatus.PendingModeration:
|
||||||
|
case PageStatus.Rejected:
|
||||||
|
// Páginas em desenvolvimento/moderação requerem preview token
|
||||||
|
var previewToken = context.Request.Query["preview"].FirstOrDefault();
|
||||||
|
if (string.IsNullOrEmpty(previewToken) ||
|
||||||
|
string.IsNullOrEmpty(page.PreviewToken) ||
|
||||||
|
previewToken != page.PreviewToken ||
|
||||||
|
page.PreviewTokenExpiry < DateTime.UtcNow)
|
||||||
|
{
|
||||||
|
_logger.LogInformation($"Page {category}/{slug} requires valid preview token");
|
||||||
|
context.Response.StatusCode = 404;
|
||||||
|
await context.Response.WriteAsync("Página em desenvolvimento. Acesso restrito.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case PageStatus.Active:
|
case PageStatus.Active:
|
||||||
// Continuar processamento normal
|
// Continuar processamento normal
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Status desconhecido - tratar como inativo
|
||||||
|
_logger.LogWarning($"Unknown page status: {page.Status} for page {category}/{slug}");
|
||||||
|
context.Response.StatusCode = 404;
|
||||||
|
await context.Response.WriteAsync("Página temporariamente indisponível.");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -125,5 +125,6 @@ public enum PageStatus
|
|||||||
PendingPayment, // Pagamento atrasado -> aviso na página
|
PendingPayment, // Pagamento atrasado -> aviso na página
|
||||||
Inactive, // Pausada pelo usuário
|
Inactive, // Pausada pelo usuário
|
||||||
PendingModeration = 4, // Aguardando moderação
|
PendingModeration = 4, // Aguardando moderação
|
||||||
Rejected = 5 // Rejeitada na moderação
|
Rejected = 5, // Rejeitada na moderação
|
||||||
|
Creating = 6 // Em desenvolvimento/criação
|
||||||
}
|
}
|
||||||
@ -23,14 +23,14 @@
|
|||||||
|
|
||||||
<!-- Lista de Páginas -->
|
<!-- Lista de Páginas -->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@foreach (var page in Model.UserPages)
|
@foreach (var pageItem in Model.UserPages)
|
||||||
{
|
{
|
||||||
<div class="col-md-6 col-lg-4 mb-3">
|
<div class="col-md-6 col-lg-4 mb-3">
|
||||||
<div class="card h-100 @(page.Status == BCards.Web.ViewModels.PageStatus.Active ? "" : "border-warning")">
|
<div class="card h-100 @(pageItem.Status == BCards.Web.ViewModels.PageStatus.Active ? "" : "border-warning")">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h6 class="card-title">
|
<h6 class="card-title">
|
||||||
@(page.DisplayName)
|
@(pageItem.DisplayName)
|
||||||
<form method="post" action="/Admin/DeletePage/@(page.Id)" style="display: inline;" onsubmit="return confirm('Tem certeza que deseja excluir esta página?')">
|
<form method="post" action="/Admin/DeletePage/@(pageItem.Id)" style="display: inline;" onsubmit="return confirm('Tem certeza que deseja excluir esta página?')">
|
||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
<button type="submit" class="btn btn-link text-danger p-0" title="Excluir página"
|
<button type="submit" class="btn btn-link text-danger p-0" title="Excluir página"
|
||||||
style="font-size: 12px; text-decoration: none;">
|
style="font-size: 12px; text-decoration: none;">
|
||||||
@ -38,16 +38,16 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</h6>
|
</h6>
|
||||||
<p class="text-muted small mb-2">@(page.Category)/@(page.Slug)</p>
|
<p class="text-muted small mb-2">@(pageItem.Category)/@(pageItem.Slug)</p>
|
||||||
|
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
@{
|
@{
|
||||||
var pageStatus = page.Status;
|
var pageStatus = pageItem.Status;
|
||||||
if (page.Status == BCards.Web.ViewModels.PageStatus.Inactive)
|
if (pageItem.Status == BCards.Web.ViewModels.PageStatus.Inactive)
|
||||||
{
|
{
|
||||||
if (page.LastModerationStatus.HasValue)
|
if (pageItem.LastModerationStatus.HasValue)
|
||||||
{
|
{
|
||||||
pageStatus = page.LastModerationStatus.Value;
|
pageStatus = pageItem.LastModerationStatus.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -71,52 +71,102 @@
|
|||||||
case BCards.Web.ViewModels.PageStatus.Rejected:
|
case BCards.Web.ViewModels.PageStatus.Rejected:
|
||||||
<span class="badge bg-danger">Rejeitada</span>
|
<span class="badge bg-danger">Rejeitada</span>
|
||||||
break;
|
break;
|
||||||
|
case BCards.Web.ViewModels.PageStatus.Creating:
|
||||||
|
<span class="badge bg-info">
|
||||||
|
<i class="fas fa-edit me-1"></i>Criando
|
||||||
|
</span>
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@if (Model.CurrentPlan.AllowsAnalytics)
|
@if (Model.CurrentPlan.AllowsAnalytics)
|
||||||
{
|
{
|
||||||
<div class="row text-center small mb-3">
|
<div class="row text-center small mb-3">
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<div class="text-primary fw-bold">@(page.TotalViews)</div>
|
<div class="text-primary fw-bold">@(pageItem.TotalViews)</div>
|
||||||
<div class="text-muted">Visualizações</div>
|
<div class="text-muted">Visualizações</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<div class="text-success fw-bold">@(page.TotalClicks)</div>
|
<div class="text-success fw-bold">@(pageItem.TotalClicks)</div>
|
||||||
<div class="text-muted">Cliques</div>
|
<div class="text-muted">Cliques</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="d-flex gap-1 flex-wrap">
|
<div class="d-flex gap-1 flex-wrap" data-page-id="@pageItem.Id" data-status="@pageItem.Status">
|
||||||
<a href="@Url.Action("ManagePage", new { id = page.Id })"
|
<!-- Botão Editar - sempre presente -->
|
||||||
class="btn btn-sm btn-outline-primary flex-fill">Editar</a>
|
<a href="@Url.Action("ManagePage", new { id = pageItem.Id })"
|
||||||
<a href="@(page.PublicUrl)" target="_blank"
|
class="btn btn-sm btn-outline-primary">
|
||||||
class="btn btn-sm btn-outline-success flex-fill">Ver</a>
|
<i class="fas fa-edit me-1"></i>Editar
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Botões condicionais por status -->
|
||||||
|
@if (pageItem.Status == BCards.Web.ViewModels.PageStatus.Creating ||
|
||||||
|
pageItem.Status == BCards.Web.ViewModels.PageStatus.Rejected)
|
||||||
|
{
|
||||||
|
<!-- Preview para páginas em desenvolvimento -->
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-outline-info"
|
||||||
|
onclick="openPreview('@pageItem.Id')"
|
||||||
|
data-page-category="@pageItem.Category"
|
||||||
|
data-page-slug="@pageItem.Slug">
|
||||||
|
<i class="fas fa-eye me-1"></i>Preview
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Botão Enviar para Moderação -->
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-success"
|
||||||
|
onclick="submitForModeration('@pageItem.Id')"
|
||||||
|
data-page-name="@pageItem.DisplayName">
|
||||||
|
<i class="fas fa-paper-plane me-1"></i>Enviar
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
else if (pageItem.Status == BCards.Web.ViewModels.PageStatus.PendingModeration)
|
||||||
|
{
|
||||||
|
<!-- Preview para páginas em moderação -->
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-outline-warning"
|
||||||
|
onclick="openPreview('@pageItem.Id')"
|
||||||
|
data-page-category="@pageItem.Category"
|
||||||
|
data-page-slug="@pageItem.Slug">
|
||||||
|
<i class="fas fa-clock me-1"></i>Preview
|
||||||
|
</button>
|
||||||
|
<span class="btn btn-sm btn-outline-secondary disabled">
|
||||||
|
<i class="fas fa-hourglass-half me-1"></i>Aguardando
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
else if (pageItem.Status == BCards.Web.ViewModels.PageStatus.Active)
|
||||||
|
{
|
||||||
|
<!-- Ver página ativa -->
|
||||||
|
<a href="/page/@pageItem.Category/@pageItem.Slug" target="_blank"
|
||||||
|
class="btn btn-sm btn-outline-success">
|
||||||
|
<i class="fas fa-external-link-alt me-1"></i>Ver
|
||||||
|
</a>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer bg-transparent">
|
<div class="card-footer bg-transparent">
|
||||||
<small class="text-muted">Criada em @(page.CreatedAt.ToString("dd/MM/yyyy"))</small>
|
<small class="text-muted">Criada em @(pageItem.CreatedAt.ToString("dd/MM/yyyy"))</small>
|
||||||
@if ((page.LastModerationStatus ?? page.Status) == BCards.Web.ViewModels.PageStatus.Rejected && !string.IsNullOrEmpty(page.Motive))
|
@if ((pageItem.LastModerationStatus ?? pageItem.Status) == BCards.Web.ViewModels.PageStatus.Rejected && !string.IsNullOrEmpty(pageItem.Motive))
|
||||||
{
|
{
|
||||||
<div class="alert alert-danger alert-dismissible fade show mt-2" role="alert">
|
<div class="alert alert-danger alert-dismissible fade show mt-2" role="alert">
|
||||||
<div class="d-flex align-items-start">
|
<div class="d-flex align-items-start">
|
||||||
<i class="fas fa-exclamation-triangle me-2 mt-1"></i>
|
<i class="fas fa-exclamation-triangle me-2 mt-1"></i>
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<strong>Motivo da rejeição:</strong><br>
|
<strong>Motivo da rejeição:</strong><br>
|
||||||
<small>@(page.Motive)</small>
|
<small>@(pageItem.Motive)</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else if (page.LastModerationStatus == BCards.Web.ViewModels.PageStatus.Active && !string.IsNullOrEmpty(page.Motive))
|
else if (pageItem.LastModerationStatus == BCards.Web.ViewModels.PageStatus.Active && !string.IsNullOrEmpty(pageItem.Motive))
|
||||||
{
|
{
|
||||||
<div class="alert alert-info alert-dismissible fade show mt-2" role="alert">
|
<div class="alert alert-info alert-dismissible fade show mt-2" role="alert">
|
||||||
<div class="d-flex align-items-start">
|
<div class="d-flex align-items-start">
|
||||||
<i class="fas fa-exclamation-triangle me-2 mt-1"></i>
|
<i class="fas fa-exclamation-triangle me-2 mt-1"></i>
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<strong>Motivo:</strong><br>
|
<strong>Motivo:</strong><br>
|
||||||
<small>@(page.Motive)</small>
|
<small>@(pageItem.Motive)</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
@ -322,6 +372,140 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<script>
|
||||||
|
// Função para abrir preview com token fresh
|
||||||
|
async function openPreview(pageId) {
|
||||||
|
const button = event.target.closest('button');
|
||||||
|
const category = button.dataset.pageCategory;
|
||||||
|
const slug = button.dataset.pageSlug;
|
||||||
|
|
||||||
|
// Desabilitar botão temporariamente
|
||||||
|
const originalText = button.innerHTML;
|
||||||
|
button.disabled = true;
|
||||||
|
button.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Carregando...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Gerar novo token
|
||||||
|
const response = await fetch(`/Admin/GeneratePreviewToken/${pageId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// Abrir preview em nova aba com token novo
|
||||||
|
const previewUrl = `${window.location.origin}/page/${category}/${slug}?preview=${result.previewToken}`;
|
||||||
|
window.open(previewUrl, '_blank');
|
||||||
|
} else {
|
||||||
|
showToast(result.message || 'Erro ao gerar preview', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao gerar preview:', error);
|
||||||
|
showToast('Erro ao gerar preview. Tente novamente.', 'error');
|
||||||
|
} finally {
|
||||||
|
// Reabilitar botão
|
||||||
|
button.disabled = false;
|
||||||
|
button.innerHTML = originalText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitForModeration(pageId) {
|
||||||
|
const pageName = event.target.dataset.pageName || 'esta página';
|
||||||
|
|
||||||
|
if (!confirm(`Enviar "${pageName}" para moderação?\n\nApós enviar, você não poderá mais editá-la até receber o resultado da análise.`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desabilitar botão durante envio
|
||||||
|
const button = event.target;
|
||||||
|
const originalText = button.innerHTML;
|
||||||
|
button.disabled = true;
|
||||||
|
button.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Enviando...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/Admin/SubmitForModeration/${pageId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// Mostrar toast de sucesso
|
||||||
|
showToast(result.message, 'success');
|
||||||
|
|
||||||
|
// Recarregar página após 2 segundos
|
||||||
|
setTimeout(() => {
|
||||||
|
location.reload();
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
showToast(result.message || 'Erro ao enviar página', 'error');
|
||||||
|
|
||||||
|
// Reabilitar botão
|
||||||
|
button.disabled = false;
|
||||||
|
button.innerHTML = originalText;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro:', error);
|
||||||
|
showToast('Erro ao enviar página para moderação', 'error');
|
||||||
|
|
||||||
|
// Reabilitar botão
|
||||||
|
button.disabled = false;
|
||||||
|
button.innerHTML = originalText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showToast(message, type) {
|
||||||
|
const toastContainer = getOrCreateToastContainer();
|
||||||
|
|
||||||
|
const bgClass = type === 'success' ? 'bg-success' : type === 'error' ? 'bg-danger' : 'bg-info';
|
||||||
|
const icon = type === 'success' ? 'fa-check-circle' : type === 'error' ? 'fa-exclamation-triangle' : 'fa-info-circle';
|
||||||
|
|
||||||
|
const toastHtml = `
|
||||||
|
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-bs-delay="5000">
|
||||||
|
<div class="toast-header ${bgClass} text-white">
|
||||||
|
<i class="fas ${icon} me-2"></i>
|
||||||
|
<strong class="me-auto">${type === 'success' ? 'Sucesso' : type === 'error' ? 'Erro' : 'Informação'}</strong>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast"></button>
|
||||||
|
</div>
|
||||||
|
<div class="toast-body">${message}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
|
||||||
|
|
||||||
|
const newToast = toastContainer.lastElementChild;
|
||||||
|
const toast = new bootstrap.Toast(newToast);
|
||||||
|
toast.show();
|
||||||
|
|
||||||
|
// Remover toast após ser fechado
|
||||||
|
newToast.addEventListener('hidden.bs.toast', function() {
|
||||||
|
newToast.remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOrCreateToastContainer() {
|
||||||
|
let container = document.querySelector('.toast-container');
|
||||||
|
if (!container) {
|
||||||
|
container = document.createElement('div');
|
||||||
|
container.className = 'toast-container position-fixed top-0 end-0 p-3';
|
||||||
|
container.style.zIndex = '1055';
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
|
||||||
@if (TempData["Success"] != null)
|
@if (TempData["Success"] != null)
|
||||||
{
|
{
|
||||||
<div class="toast-container position-fixed top-0 end-0 p-3">
|
<div class="toast-container position-fixed top-0 end-0 p-3">
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
@{
|
@{
|
||||||
var isPreview = ViewBag.IsPreview as bool? ?? false;
|
//var isPreview = ViewBag.IsPreview as bool? ?? false;
|
||||||
ViewData["Title"] = "BCards - Crie seu LinkTree Profissional";
|
ViewData["Title"] = "BCards - Crie seu LinkTree Profissional";
|
||||||
var categories = ViewBag.Categories as List<BCards.Web.Models.Category> ?? new List<BCards.Web.Models.Category>();
|
var categories = ViewBag.Categories as List<BCards.Web.Models.Category> ?? new List<BCards.Web.Models.Category>();
|
||||||
var recentPages = ViewBag.RecentPages as List<BCards.Web.Models.UserPage> ?? new List<BCards.Web.Models.UserPage>();
|
var recentPages = ViewBag.RecentPages as List<BCards.Web.Models.UserPage> ?? new List<BCards.Web.Models.UserPage>();
|
||||||
Layout = isPreview ? "_Layout" : "_UserPageLayout";
|
//Layout = isPreview ? "_Layout" : "_UserPageLayout";
|
||||||
|
Layout = "_Layout";
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="hero-section bg-primary bg-gradient text-white py-5 mb-5">
|
<div class="hero-section bg-primary bg-gradient text-white py-5 mb-5">
|
||||||
@ -26,9 +27,6 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<a asp-controller="Auth" asp-action="Login" class="btn btn-outline-light btn-lg px-4">
|
|
||||||
Entrar
|
|
||||||
</a>
|
|
||||||
<a asp-controller="Auth" asp-action="Login" class="btn btn-light btn-lg px-4">
|
<a asp-controller="Auth" asp-action="Login" class="btn btn-light btn-lg px-4">
|
||||||
Começar Grátis
|
Começar Grátis
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Planos e Preços - BCards";
|
ViewData["Title"] = "Planos e Preços - BCards";
|
||||||
var isPreview = ViewBag.IsPreview as bool? ?? false;
|
//var isPreview = ViewBag.IsPreview as bool? ?? false;
|
||||||
Layout = isPreview ? "_Layout" : "_UserPageLayout";
|
//Layout = isPreview ? "_Layout" : "_UserPageLayout";
|
||||||
|
Layout = "_Layout";
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="container py-5">
|
<div class="container py-5">
|
||||||
|
|||||||
@ -3,7 +3,10 @@
|
|||||||
@if (Model)
|
@if (Model)
|
||||||
{
|
{
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link dropdown-toggle text-warning fw-bold" href="#" id="moderationDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
<a class="nav-link dropdown-toggle @(ViewBag.IsHomePage == true ? "text-warning" : "text-warning") fw-bold"
|
||||||
|
href="#" id="moderationDropdown" role="button"
|
||||||
|
data-bs-toggle="dropdown" aria-expanded="false"
|
||||||
|
style="@(ViewBag.IsHomePage == true ? "color: #fbbf24 !important;" : "")">
|
||||||
<i class="fas fa-shield-alt me-1"></i>Moderação
|
<i class="fas fa-shield-alt me-1"></i>Moderação
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu" aria-labelledby="moderationDropdown">
|
<ul class="dropdown-menu" aria-labelledby="moderationDropdown">
|
||||||
|
|||||||
@ -40,37 +40,54 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
|
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light fixed-top @(ViewBag.IsHomePage == true ? "bg-home-blue" : "bg-dashboard")" id="mainNavbar">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand fw-bold text-primary" asp-area="" asp-controller="Home" asp-action="Index">
|
<a class="navbar-brand fw-bold @(ViewBag.IsHomePage == true ? "text-white" : "text-primary")"
|
||||||
|
asp-area="" asp-controller="Home" asp-action="Index">
|
||||||
BCards
|
BCards
|
||||||
</a>
|
</a>
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
|
|
||||||
aria-expanded="false" aria-label="Toggle navigation">
|
<button class="navbar-toggler @(ViewBag.IsHomePage == true ? "navbar-dark" : "")"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target=".navbar-collapse"
|
||||||
|
aria-controls="navbarSupportedContent"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-label="Toggle navigation">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
|
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
|
||||||
<ul class="navbar-nav flex-grow-1">
|
<ul class="navbar-nav flex-grow-1">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Início</a>
|
<a class="nav-link @(ViewBag.IsHomePage == true ? "text-white" : "text-dark")"
|
||||||
|
asp-area="" asp-controller="Home" asp-action="Index">
|
||||||
|
Início
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Pricing">Planos</a>
|
<a class="nav-link @(ViewBag.IsHomePage == true ? "text-white" : "text-dark")"
|
||||||
|
asp-area="" asp-controller="Home" asp-action="Pricing">
|
||||||
|
Planos
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
@* Menu de Moderação via ViewComponent *@
|
@* Menu de Moderação via ViewComponent *@
|
||||||
@await Component.InvokeAsync("ModerationMenu")
|
@await Component.InvokeAsync("ModerationMenu")
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
@if (User.Identity?.IsAuthenticated == true)
|
@if (User.Identity?.IsAuthenticated == true)
|
||||||
{
|
{
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link text-dark" asp-area="" asp-controller="Admin" asp-action="Dashboard">
|
<a class="nav-link @(ViewBag.IsHomePage == true ? "text-white" : "text-dark")"
|
||||||
|
asp-area="" asp-controller="Admin" asp-action="Dashboard">
|
||||||
<i class="fas fa-user me-1"></i>Dashboard
|
<i class="fas fa-user me-1"></i>Dashboard
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link text-dark" asp-area="" asp-controller="Auth" asp-action="Logout">
|
<a class="nav-link @(ViewBag.IsHomePage == true ? "text-white" : "text-dark")"
|
||||||
|
asp-area="" asp-controller="Auth" asp-action="Logout">
|
||||||
<i class="fas fa-sign-out-alt me-1"></i>Sair
|
<i class="fas fa-sign-out-alt me-1"></i>Sair
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -78,7 +95,8 @@
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link text-dark" asp-area="" asp-controller="Auth" asp-action="Login">
|
<a class="nav-link @(ViewBag.IsHomePage == true ? "text-white" : "text-dark")"
|
||||||
|
asp-area="" asp-controller="Auth" asp-action="Login">
|
||||||
<i class="fas fa-sign-in-alt me-1"></i>Entrar
|
<i class="fas fa-sign-in-alt me-1"></i>Entrar
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@ -19,6 +19,7 @@ html {
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
margin-bottom: 60px;
|
margin-bottom: 60px;
|
||||||
|
padding-top: 70px; /* Altura da navbar fixa */
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
@ -217,3 +218,56 @@ body {
|
|||||||
border: 2px solid;
|
border: 2px solid;
|
||||||
border-image: linear-gradient(135deg, #667eea 0%, #764ba2 100%) 1;
|
border-image: linear-gradient(135deg, #667eea 0%, #764ba2 100%) 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Menu Home (mesmo gradiente do fundo principal) */
|
||||||
|
.bg-home-blue {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
||||||
|
/* Removida linha divisória na home */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Menu Dashboard (mais neutro) */
|
||||||
|
.bg-dashboard {
|
||||||
|
background-color: #ffffff !important;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
body {
|
||||||
|
padding-top: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-home-blue .navbar-nav {
|
||||||
|
background: rgba(102, 126, 234, 0.95);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hamburger menu para home */
|
||||||
|
.navbar-dark .navbar-toggler-icon {
|
||||||
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.85%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover effects para home menu */
|
||||||
|
.bg-home-blue .nav-link:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover effects para dashboard menu */
|
||||||
|
.bg-dashboard .nav-link:hover {
|
||||||
|
background-color: rgba(37, 99, 235, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Garantir contraste adequado no menu home */
|
||||||
|
.bg-home-blue .navbar-brand,
|
||||||
|
.bg-home-blue .nav-link {
|
||||||
|
color: #ffffff !important;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user