generated from ricardo/MVCLogin
feat: tela para cadastrar nova postagem
This commit is contained in:
parent
4b04639ad7
commit
955a131fec
BIN
LogoPoost.png
Normal file
BIN
LogoPoost.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
17
Postall.Domain/Dtos/Responses/FacebookTokenResponse.cs
Normal file
17
Postall.Domain/Dtos/Responses/FacebookTokenResponse.cs
Normal file
@ -0,0 +1,17 @@
|
||||
// Models/FacebookTokenResponse.cs
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Postall.Domain.Dtos.Responses
|
||||
{
|
||||
public class FacebookTokenResponse
|
||||
{
|
||||
[JsonPropertyName("access_token")]
|
||||
public string AccessToken { get; set; }
|
||||
|
||||
[JsonPropertyName("token_type")]
|
||||
public string TokenType { get; set; }
|
||||
|
||||
[JsonPropertyName("expires_in")]
|
||||
public long ExpiresIn { get; set; }
|
||||
}
|
||||
}
|
||||
14
Postall.Domain/Entities/FacebookToken.cs
Normal file
14
Postall.Domain/Entities/FacebookToken.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Postall.Domain.Entities
|
||||
{
|
||||
public class FacebookToken
|
||||
{
|
||||
public string AccessToken { get; set; }
|
||||
public DateTime ExpiresAt { get; set; }
|
||||
}
|
||||
}
|
||||
20
Postall.Domain/Entities/UserSocialData.cs
Normal file
20
Postall.Domain/Entities/UserSocialData.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||||
|
||||
namespace Postall.Domain.Entities
|
||||
{
|
||||
public class UserSocialData
|
||||
{
|
||||
[BsonId]
|
||||
public ObjectId Id { get; set; }
|
||||
public string UserId { get; set; }
|
||||
public string GoogleToken { get; set; }
|
||||
public FacebookToken FacebookToken { get; set; }
|
||||
}
|
||||
}
|
||||
@ -7,6 +7,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MongoDB.Bson" Version="3.1.0" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="3.1.0" />
|
||||
<PackageReference Include="Serilog" Version="4.0.2" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||
<PackageReference Include="Serilog.Enrichers.Context" Version="4.6.5" />
|
||||
@ -15,7 +17,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\vcart.me\vcart.back\Struct.ValueObjects\BaseDomain.csproj" />
|
||||
<ProjectReference Include="..\BaseDomain\BaseDomain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
14
Postall.Domain/Services/IFacebookServices.cs
Normal file
14
Postall.Domain/Services/IFacebookServices.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Postall.Domain.Services
|
||||
{
|
||||
public interface IFacebookServices
|
||||
{
|
||||
Task<string> GetLongLivedToken(string shortLivedToken);
|
||||
Task SaveFacebookToken(string userId, string token);
|
||||
}
|
||||
}
|
||||
@ -7,8 +7,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MongoDB.Bson" Version="2.28.0" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="2.28.0" />
|
||||
<PackageReference Include="MongoDB.Bson" Version="3.1.0" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="3.1.0" />
|
||||
<PackageReference Include="Serilog" Version="4.0.2" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||
<PackageReference Include="Serilog.Enrichers.Context" Version="4.6.5" />
|
||||
@ -16,4 +16,8 @@
|
||||
<PackageReference Include="Serilog.Sinks.Grafana.Loki" Version="8.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Postall.Domain\Postall.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
49
Postall.Infra/Services/FacebookTokenService.cs
Normal file
49
Postall.Infra/Services/FacebookTokenService.cs
Normal file
@ -0,0 +1,49 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using MongoDB.Driver;
|
||||
using Postall.Domain.Dtos.Responses;
|
||||
using Postall.Domain.Entities;
|
||||
using Postall.Domain.Services;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Postall.Infra.Services
|
||||
{
|
||||
public class FacebookTokenService: IFacebookServices
|
||||
{
|
||||
private readonly IMongoCollection<UserSocialData> _tokens;
|
||||
private readonly IConfiguration _config;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public async Task<string> GetLongLivedToken(string shortLivedToken)
|
||||
{
|
||||
var appId = _config["Authentication:Facebook:AppId"];
|
||||
var appSecret = _config["Authentication:Facebook:AppSecret"];
|
||||
|
||||
var response = await _httpClient.GetFromJsonAsync<FacebookTokenResponse>(
|
||||
$"https://graph.facebook.com/oauth/access_token?" +
|
||||
$"grant_type=fb_exchange_token&" +
|
||||
$"client_id={appId}&" +
|
||||
$"client_secret={appSecret}&" +
|
||||
$"fb_exchange_token={shortLivedToken}");
|
||||
|
||||
return response.AccessToken;
|
||||
}
|
||||
|
||||
public async Task SaveFacebookToken(string userId, string token)
|
||||
{
|
||||
var update = Builders<UserSocialData>.Update
|
||||
.Set(x => x.FacebookToken.AccessToken, token)
|
||||
.Set(x => x.FacebookToken.ExpiresAt, DateTime.UtcNow.AddDays(60));
|
||||
|
||||
await _tokens.UpdateOneAsync(
|
||||
x => x.UserId == userId,
|
||||
update,
|
||||
new UpdateOptions { IsUpsert = true }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,11 +2,19 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Facebook;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Postall.Domain.Services;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Postall.Controllers
|
||||
{
|
||||
public class OtherLoginsController : Controller
|
||||
{
|
||||
private readonly IFacebookServices _facebookServices;
|
||||
|
||||
public OtherLoginsController(IFacebookServices facebookServices)
|
||||
{
|
||||
this._facebookServices = facebookServices;
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult Index()
|
||||
{
|
||||
@ -24,21 +32,13 @@ namespace Postall.Controllers
|
||||
public async Task<IActionResult> FacebookResponse()
|
||||
{
|
||||
var result = await HttpContext.AuthenticateAsync(FacebookDefaults.AuthenticationScheme);
|
||||
if (!result.Succeeded) return RedirectToAction("Login");
|
||||
|
||||
if (!result.Succeeded)
|
||||
return RedirectToAction("Login");
|
||||
var accessToken = result.Properties.GetTokenValue("access_token");
|
||||
var longLivedToken = await _facebookServices.GetLongLivedToken(accessToken);
|
||||
|
||||
var claims = result.Principal.Identities.FirstOrDefault()
|
||||
.Claims.Select(claim => new
|
||||
{
|
||||
claim.Issuer,
|
||||
claim.OriginalIssuer,
|
||||
claim.Type,
|
||||
claim.Value
|
||||
});
|
||||
|
||||
// Aqui você pode implementar sua lógica de login
|
||||
// Por exemplo, criar ou atualizar o usuário no banco de dados
|
||||
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
await _facebookServices.SaveFacebookToken(userId, longLivedToken);
|
||||
|
||||
return RedirectToAction("Index", "Home");
|
||||
}
|
||||
|
||||
91
Postall/Controllers/SocialMediaController.cs
Normal file
91
Postall/Controllers/SocialMediaController.cs
Normal file
@ -0,0 +1,91 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Postall.Models;
|
||||
|
||||
namespace Postall.Controllers
|
||||
{
|
||||
public class SocialMediaController : Controller
|
||||
{
|
||||
[HttpGet]
|
||||
public IActionResult Index()
|
||||
{
|
||||
// Implementar lógica para buscar lista de posts
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public JsonResult GetPostDetails(int postId)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Implementar lógica para buscar detalhes do post
|
||||
return Json(new { success = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Json(new { success = false, message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public JsonResult GetSocialMediaStatus(int postId)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Implementar lógica para buscar status das redes sociais
|
||||
return Json(new { success = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Json(new { success = false, message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult Post()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public JsonResult SaveDraft([FromBody] PostViewModel model)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Lógica para salvar rascunho
|
||||
return Json(new { success = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Json(new { success = false, message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public JsonResult PublishPost([FromBody] PostViewModel model)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Lógica para publicar post
|
||||
return Json(new { success = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Json(new { success = false, message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public JsonResult SchedulePost([FromBody] PostScheduleViewModel model)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Lógica para agendar post
|
||||
return Json(new { success = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Json(new { success = false, message = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
Postall/Models/PostListViewModel.cs
Normal file
12
Postall/Models/PostListViewModel.cs
Normal file
@ -0,0 +1,12 @@
|
||||
namespace Postall.Models
|
||||
{
|
||||
public class PostListViewModel
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Channel { get; set; }
|
||||
public string Title { get; set; }
|
||||
public DateTime? NextScheduledDate { get; set; }
|
||||
public DateTime LastUpdate { get; set; }
|
||||
public List<SocialMediaStatusViewModel> SocialMediaStatus { get; set; }
|
||||
}
|
||||
}
|
||||
9
Postall/Models/PostScheduleViewModel.cs
Normal file
9
Postall/Models/PostScheduleViewModel.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Postall.Models
|
||||
{
|
||||
public class PostScheduleViewModel
|
||||
{
|
||||
public bool IsManual { get; set; }
|
||||
public DayOfWeek? WeekDay { get; set; }
|
||||
public TimeSpan? Time { get; set; }
|
||||
}
|
||||
}
|
||||
10
Postall/Models/PostViewModel.cs
Normal file
10
Postall/Models/PostViewModel.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace Postall.Models
|
||||
{
|
||||
public class PostViewModel
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string Content { get; set; }
|
||||
public string ImageUrl { get; set; }
|
||||
public List<string> SelectedPlatforms { get; set; }
|
||||
}
|
||||
}
|
||||
9
Postall/Models/SocialMediaStatusViewModel.cs
Normal file
9
Postall/Models/SocialMediaStatusViewModel.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Postall.Models
|
||||
{
|
||||
public class SocialMediaStatusViewModel
|
||||
{
|
||||
public string Platform { get; set; }
|
||||
public DateTime? NextScheduledDate { get; set; }
|
||||
public string Status { get; set; }
|
||||
}
|
||||
}
|
||||
@ -32,6 +32,11 @@
|
||||
<Folder Include="wwwroot\img\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Postall.Domain\Postall.Domain.csproj" />
|
||||
<ProjectReference Include="..\Postall.Infra\Postall.Infra.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Resource.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
|
||||
@ -4,6 +4,8 @@ using Microsoft.AspNetCore.Authentication.Google;
|
||||
using Microsoft.AspNetCore.Localization;
|
||||
using Microsoft.AspNetCore.Mvc.Razor;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Postall.Domain.Services;
|
||||
using Postall.Infra.Services;
|
||||
using Serilog;
|
||||
using Serilog.Sinks.Grafana.Loki;
|
||||
using Stripe;
|
||||
@ -86,6 +88,8 @@ builder.Services.AddControllersWithViews();
|
||||
builder.Services.AddHttpClient();
|
||||
builder.Services.AddSerilog();
|
||||
|
||||
builder.Services.AddScoped<IFacebookServices, FacebookTokenService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
var locOptions = app.Services.GetService<IOptions<RequestLocalizationOptions>>();
|
||||
|
||||
@ -1,11 +1,18 @@
|
||||
@{
|
||||
ViewData["Title"] = "Site";
|
||||
ViewData["Title"] = "Sites";
|
||||
}
|
||||
|
||||
<div class="text-center">
|
||||
<h1 class="display-4">Login</h1>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-4">
|
||||
|
||||
<div class="container">
|
||||
<div class="row justify-content-center align-items-center min-vh-100">
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-lg">
|
||||
<div class="card-body text-center p-5">
|
||||
<h2 class="card-title mb-4">Bem-vindo</h2>
|
||||
<p class="card-text text-muted mb-4">
|
||||
Faça login com sua conta Microsoft para acessar o chat.
|
||||
</p>
|
||||
|
||||
<form asp-action="FacebookLogin" method="get">
|
||||
<button type="submit" class="btn btn-primary btn-block">
|
||||
<i class="fab fa-facebook"></i> Login com Facebook
|
||||
@ -14,3 +21,34 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Styles {
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
}
|
||||
|
||||
.card {
|
||||
border: none;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #2f2f2f;
|
||||
border: none;
|
||||
padding: 12px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #404040;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
</style>
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
||||
}
|
||||
|
||||
@ -32,6 +32,15 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white" asp-area="" asp-controller="Plans" asp-action="Index">Planos</a>
|
||||
</li>
|
||||
@if (User!=null && User.Identity!=null && User.Identity.IsAuthenticated)
|
||||
{
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white" asp-area="" asp-controller="OtherLogins" asp-action="Index">Sites</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white" asp-area="" asp-controller="SocialMedia" asp-action="Index">Postagens</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
@if (User!=null && User.Identity!=null && !User.Identity.IsAuthenticated)
|
||||
{
|
||||
@ -44,11 +53,6 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<ul class="navbar-nav ml-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white" asp-area="" asp-controller="OtherLogins" asp-action="Index">Sites</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav ml-auto">
|
||||
<partial name="_Language"/>
|
||||
<li class="nav-item dropdown" style="margin-right: 10px">
|
||||
|
||||
254
Postall/Views/SocialMedia/Index.cshtml
Normal file
254
Postall/Views/SocialMedia/Index.cshtml
Normal file
@ -0,0 +1,254 @@
|
||||
@using Postall.Models
|
||||
|
||||
<!-- Views/SocialMedia/Index.cshtml -->
|
||||
@{
|
||||
ViewData["Title"] = "Gerenciador de Postagens";
|
||||
}
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2>Gerenciador de Postagens</h2>
|
||||
<a href="@Url.Action("Post", "SocialMedia")" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Nova Postagem
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover" id="postsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Canal</th>
|
||||
<th>Título</th>
|
||||
<th>Próxima Data</th>
|
||||
<th>Última Atualização</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Os dados serão preenchidos via JavaScript -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Template para detalhes expandidos -->
|
||||
<template id="detailsTemplate">
|
||||
<div class="expanded-details p-3">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h5 class="mb-3">Status das Redes Sociais</h5>
|
||||
<div class="social-media-grid">
|
||||
<!-- Facebook -->
|
||||
<div class="social-media-item">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="bi bi-facebook me-2"></i>
|
||||
<span class="platform-name">Facebook</span>
|
||||
</div>
|
||||
<div class="status-badge">
|
||||
<span class="badge rounded-pill status-placeholder">Status</span>
|
||||
</div>
|
||||
<div class="next-date small text-muted mt-1">
|
||||
Próxima data: <span class="date-placeholder">--/--/----</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Instagram -->
|
||||
<div class="social-media-item">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="bi bi-instagram me-2"></i>
|
||||
<span class="platform-name">Instagram</span>
|
||||
</div>
|
||||
<div class="status-badge">
|
||||
<span class="badge rounded-pill status-placeholder">Status</span>
|
||||
</div>
|
||||
<div class="next-date small text-muted mt-1">
|
||||
Próxima data: <span class="date-placeholder">--/--/----</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Twitter/X -->
|
||||
<div class="social-media-item">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="bi bi-twitter-x me-2"></i>
|
||||
<span class="platform-name">Twitter/X</span>
|
||||
</div>
|
||||
<div class="status-badge">
|
||||
<span class="badge rounded-pill status-placeholder">Status</span>
|
||||
</div>
|
||||
<div class="next-date small text-muted mt-1">
|
||||
Próxima data: <span class="date-placeholder">--/--/----</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WhatsApp -->
|
||||
<div class="social-media-item">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="bi bi-whatsapp me-2"></i>
|
||||
<span class="platform-name">WhatsApp</span>
|
||||
</div>
|
||||
<div class="status-badge">
|
||||
<span class="badge rounded-pill status-placeholder">Status</span>
|
||||
</div>
|
||||
<div class="next-date small text-muted mt-1">
|
||||
Próxima data: <span class="date-placeholder">--/--/----</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Telegram -->
|
||||
<div class="social-media-item">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="bi bi-telegram me-2"></i>
|
||||
<span class="platform-name">Telegram</span>
|
||||
</div>
|
||||
<div class="status-badge">
|
||||
<span class="badge rounded-pill status-placeholder">Status</span>
|
||||
</div>
|
||||
<div class="next-date small text-muted mt-1">
|
||||
Próxima data: <span class="date-placeholder">--/--/----</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@section Styles {
|
||||
<style>
|
||||
.social-media-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.social-media-item {
|
||||
padding: 1rem;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.5rem;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.status-badge .badge {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.badge.status-publicado { background-color: #198754; }
|
||||
.badge.status-nao-selecionado { background-color: #6c757d; }
|
||||
.badge.status-gerado { background-color: #0dcaf0; }
|
||||
.badge.status-agendado { background-color: #ffc107; color: #000; }
|
||||
.badge.status-automatizado { background-color: #0d6efd; }
|
||||
|
||||
tr.expanded {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.expanded-details {
|
||||
background-color: #f8f9fa;
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
@@media (max-width: 768px) {
|
||||
.social-media-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
loadPosts();
|
||||
});
|
||||
|
||||
function loadPosts() {
|
||||
$.get('@Url.Action("GetPostDetails", "SocialMedia")', function(data) {
|
||||
if (data.success) {
|
||||
renderPosts(data.posts);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderPosts(posts) {
|
||||
const tbody = $('#postsTable tbody');
|
||||
tbody.empty();
|
||||
|
||||
posts.forEach(post => {
|
||||
const row = $(`
|
||||
<tr data-post-id="${post.id}">
|
||||
<td>
|
||||
<button class="btn btn-sm btn-link expand-btn">
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
</button>
|
||||
</td>
|
||||
<td>${post.channel}</td>
|
||||
<td>${post.title}</td>
|
||||
<td>${formatDate(post.nextScheduledDate)}</td>
|
||||
<td>${formatDate(post.lastUpdate)}</td>
|
||||
</tr>
|
||||
`);
|
||||
|
||||
tbody.append(row);
|
||||
});
|
||||
}
|
||||
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return '--/--/----';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('pt-BR');
|
||||
}
|
||||
|
||||
function getStatusBadgeClass(status) {
|
||||
const statusMap = {
|
||||
'publicado': 'status-publicado',
|
||||
'não selecionado': 'status-nao-selecionado',
|
||||
'gerado': 'status-gerado',
|
||||
'agendado': 'status-agendado',
|
||||
'automatizado': 'status-automatizado'
|
||||
};
|
||||
return statusMap[status.toLowerCase()] || '';
|
||||
}
|
||||
|
||||
$(document).on('click', '.expand-btn', function() {
|
||||
const row = $(this).closest('tr');
|
||||
const postId = row.data('post-id');
|
||||
const icon = $(this).find('i');
|
||||
|
||||
if (row.next().hasClass('details-row')) {
|
||||
// Fechar detalhes
|
||||
row.next().remove();
|
||||
row.removeClass('expanded');
|
||||
icon.removeClass('bi-chevron-down').addClass('bi-chevron-right');
|
||||
} else {
|
||||
// Abrir detalhes
|
||||
$.get(`@Url.Action("GetSocialMediaStatus", "SocialMedia")?postId=${postId}`, function(data) {
|
||||
if (data.success) {
|
||||
const template = document.getElementById('detailsTemplate');
|
||||
const detailsContent = template.content.cloneNode(true);
|
||||
|
||||
// Preencher os status
|
||||
data.socialMediaStatus.forEach(status => {
|
||||
const platformElement = $(detailsContent).find(`[data-platform="${status.platform}"]`);
|
||||
platformElement.find('.status-placeholder')
|
||||
.text(status.status)
|
||||
.addClass(getStatusBadgeClass(status.status));
|
||||
platformElement.find('.date-placeholder').text(formatDate(status.nextScheduledDate));
|
||||
});
|
||||
|
||||
const detailsRow = $('<tr class="details-row">').append(
|
||||
$('<td colspan="5">').append(detailsContent)
|
||||
);
|
||||
|
||||
row.addClass('expanded');
|
||||
row.after(detailsRow);
|
||||
icon.removeClass('bi-chevron-right').addClass('bi-chevron-down');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
}
|
||||
347
Postall/Views/SocialMedia/Post.cshtml
Normal file
347
Postall/Views/SocialMedia/Post.cshtml
Normal file
@ -0,0 +1,347 @@
|
||||
@using Postall.Models
|
||||
@model PostViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Gerenciar Postagens";
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- Menu lateral em desktop / Menu superior em mobile -->
|
||||
<div class="col-md-3 col-12 mb-3">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5>Plataformas</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<button type="button" class="btn btn-outline-primary platform-btn" data-platform="facebook">
|
||||
Facebook
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary platform-btn" data-platform="instagram">
|
||||
Instagram
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary platform-btn" data-platform="twitter">
|
||||
Twitter/X
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary platform-btn" data-platform="whatsapp">
|
||||
WhatsApp
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary platform-btn" data-platform="telegram">
|
||||
Telegram
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h5>Agendamento</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="radio" name="schedule" id="manual" checked>
|
||||
<label class="form-check-label" for="manual">Manual</label>
|
||||
</div>
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="radio" name="schedule" id="weekly">
|
||||
<label class="form-check-label" for="weekly">Semanal</label>
|
||||
</div>
|
||||
<div id="weeklyOptions" class="d-none">
|
||||
<div class="mb-2">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input weekday-check" type="checkbox" value="1" id="monday">
|
||||
<label class="form-check-label" for="monday">Segunda-feira</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input weekday-check" type="checkbox" value="2" id="tuesday">
|
||||
<label class="form-check-label" for="tuesday">Terça-feira</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input weekday-check" type="checkbox" value="3" id="wednesday">
|
||||
<label class="form-check-label" for="wednesday">Quarta-feira</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input weekday-check" type="checkbox" value="4" id="thursday">
|
||||
<label class="form-check-label" for="thursday">Quinta-feira</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input weekday-check" type="checkbox" value="5" id="friday">
|
||||
<label class="form-check-label" for="friday">Sexta-feira</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input weekday-check" type="checkbox" value="6" id="saturday">
|
||||
<label class="form-check-label" for="saturday">Sábado</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input weekday-check" type="checkbox" value="0" id="sunday">
|
||||
<label class="form-check-label" for="sunday">Domingo</label>
|
||||
</div>
|
||||
</div>
|
||||
<input type="time" class="form-control" id="scheduleTime">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview lateral -->
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5>Video</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Canal</label>
|
||||
<input type="text" class="form-control" id="previewChannel" readonly>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">URL do Vídeo</label>
|
||||
<input type="text" class="form-control" id="previewVideoUrl">
|
||||
</div>
|
||||
<div class="preview-content">
|
||||
<h6>Título</h6>
|
||||
<p id="previewTitle"></p>
|
||||
<h6>Conteúdo</h6>
|
||||
<p id="previewContent"></p>
|
||||
<div id="previewImage" class="mt-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<button type="button" class="btn btn-secondary me-2" onclick="savePlatformDraft()">Gerar com IA</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5>Nova Postagem</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="postTitle" class="form-label">Título</label>
|
||||
<input type="text" class="form-control" id="postTitle">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="postContent" class="form-label">Conteúdo</label>
|
||||
<textarea class="form-control" id="postContent" rows="5"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="postImage" class="form-label">Imagem</label>
|
||||
<input type="file" class="form-control" id="postImage" accept="image/*">
|
||||
</div>
|
||||
<div class="preview-image mb-3 d-none">
|
||||
<img id="imagePreview" src="#" alt="Preview" class="img-fluid">
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<button type="button" class="btn btn-secondary me-2" onclick="saveDraft()">Salvar Rascunho</button>
|
||||
<button type="button" class="btn btn-primary" onclick="publishPost()">Publicar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
let selectedPlatform = null;
|
||||
|
||||
// Gestão das plataformas
|
||||
document.querySelectorAll('.platform-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async function() {
|
||||
const platform = this.dataset.platform;
|
||||
|
||||
// Reset outros botões
|
||||
document.querySelectorAll('.platform-btn').forEach(b =>
|
||||
b.classList.replace('btn-primary', 'btn-outline-primary'));
|
||||
|
||||
// Ativar botão selecionado
|
||||
this.classList.replace('btn-outline-primary', 'btn-primary');
|
||||
selectedPlatform = platform;
|
||||
|
||||
// Carregar dados da plataforma
|
||||
await loadPlatformData(platform);
|
||||
});
|
||||
});
|
||||
|
||||
async function loadPlatformData(platform) {
|
||||
try {
|
||||
const response = await fetch(`/api/socialMedia/platformData/${platform}`);
|
||||
const data = await response.json();
|
||||
|
||||
// Atualizar preview
|
||||
document.getElementById('previewChannel').value = data.channel;
|
||||
document.getElementById('previewVideoUrl').value = data.videoUrl;
|
||||
document.getElementById('previewTitle').textContent = data.title;
|
||||
document.getElementById('previewContent').textContent = data.content;
|
||||
|
||||
if (data.imageUrl) {
|
||||
document.getElementById('previewImage').innerHTML =
|
||||
`<img src="${data.imageUrl}" class="img-fluid" alt="Preview">`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar dados da plataforma:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Mostrar/ocultar opções de agendamento semanal
|
||||
document.getElementById('weekly').addEventListener('change', function() {
|
||||
document.getElementById('weeklyOptions').classList.remove('d-none');
|
||||
});
|
||||
|
||||
document.getElementById('manual').addEventListener('change', function() {
|
||||
document.getElementById('weeklyOptions').classList.add('d-none');
|
||||
});
|
||||
|
||||
// Preview da imagem
|
||||
document.getElementById('postImage').addEventListener('change', function(e) {
|
||||
if (e.target.files && e.target.files[0]) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
document.getElementById('imagePreview').src = e.target.result;
|
||||
document.querySelector('.preview-image').classList.remove('d-none');
|
||||
};
|
||||
reader.readAsDataURL(e.target.files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
function getSelectedPlatform() {
|
||||
return selectedPlatform;
|
||||
}
|
||||
|
||||
function getSelectedWeekDays() {
|
||||
return Array.from(document.querySelectorAll('.weekday-check:checked'))
|
||||
.map(checkbox => parseInt(checkbox.value));
|
||||
}
|
||||
|
||||
async function generateAIContent() {
|
||||
if (!selectedPlatform) {
|
||||
alert('Selecione uma plataforma primeiro!');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/socialMedia/generateContent', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
platform: selectedPlatform,
|
||||
videoUrl: document.getElementById('previewVideoUrl').value
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
document.getElementById('previewTitle').textContent = data.title;
|
||||
document.getElementById('previewContent').textContent = data.content;
|
||||
} else {
|
||||
alert('Erro ao gerar conteúdo: ' + data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao gerar conteúdo:', error);
|
||||
alert('Erro ao gerar conteúdo');
|
||||
}
|
||||
}
|
||||
|
||||
async function savePlatformDraft() {
|
||||
if (!selectedPlatform) {
|
||||
alert('Selecione uma plataforma primeiro!');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
platform: selectedPlatform,
|
||||
title: document.getElementById('previewTitle').textContent,
|
||||
content: document.getElementById('previewContent').textContent,
|
||||
videoUrl: document.getElementById('previewVideoUrl').value
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/socialMedia/savePlatformDraft', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
alert('Rascunho salvo com sucesso!');
|
||||
} else {
|
||||
alert('Erro ao salvar rascunho: ' + result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao salvar rascunho:', error);
|
||||
alert('Erro ao salvar rascunho');
|
||||
}
|
||||
}
|
||||
|
||||
function publishPost() {
|
||||
const isWeekly = document.getElementById('weekly').checked;
|
||||
const data = {
|
||||
title: document.getElementById('postTitle').value,
|
||||
content: document.getElementById('postContent').value,
|
||||
platform: getSelectedPlatform()
|
||||
};
|
||||
|
||||
if (isWeekly) {
|
||||
data.isManual = false;
|
||||
data.weekDays = getSelectedWeekDays();
|
||||
data.time = document.getElementById('scheduleTime').value;
|
||||
|
||||
$.ajax({
|
||||
url: '@Url.Action("SchedulePost", "SocialMedia")',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(data),
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
alert('Post agendado com sucesso!');
|
||||
} else {
|
||||
alert('Erro ao agendar post: ' + response.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$.ajax({
|
||||
url: '@Url.Action("PublishPost", "SocialMedia")',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(data),
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
alert('Post publicado com sucesso!');
|
||||
} else {
|
||||
alert('Erro ao publicar post: ' + response.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
}
|
||||
|
||||
<style>
|
||||
.platform-btn {
|
||||
min-width: 120px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.preview-image img {
|
||||
max-height: 300px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
@@media (max-width: 768px) {
|
||||
.container-fluid {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@ -18,8 +18,8 @@
|
||||
"AppSecret": "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"
|
||||
},
|
||||
"Facebook": {
|
||||
"AppId": "seu_app_id_aqui",
|
||||
"AppSecret": "seu_app_secret_aqui"
|
||||
"AppId": "963281005306692",
|
||||
"AppSecret": "575839ccbb36d1457715f1f6dd0a8db9"
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user