feat: meus resumos com estrutura codificada

This commit is contained in:
Ricardo Carneiro 2025-04-21 23:01:55 -03:00
parent 7b3c63ff37
commit 511b538ad3
38 changed files with 2922 additions and 555 deletions

View File

@ -1,4 +1,4 @@
using SumaTube.Infra.MongoDB.Documents;
using SumaTube.Infra.MongoDB.Documents.UserPlan;
using System;
using System.Collections.Generic;
using System.Linq;

View File

@ -0,0 +1,14 @@
using SumaTube.Domain.Entities.Videos;
namespace SumaTube.Infra.Contracts.Repositories.Videos
{
public interface IVideoSummaryRepository
{
Task<VideoSummary> GetByIdAsync(string id);
Task<List<VideoSummary>> GetByUserIdAsync(string userId);
Task AddAsync(VideoSummary videoSummary);
Task UpdateAsync(VideoSummary videoSummary);
Task<bool> ExistsAsync(string videoId, string userId, string language);
Task<VideoSummary> GetByVideoIdAndUserIdAndLanguageAsync(string videoId, string userId, string language);
}
}

View File

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SumaTube.Infra
{
public class VideoProcessingCompletedMessage
{
public string SessionId { get; set; }
public string UserId { get; set; }
public bool Success { get; set; }
public string Title { get; set; }
public string Summary { get; set; }
public string Transcription { get; set; }
public int Duration { get; set; }
public string ErrorMessage { get; set; }
}
}

View File

@ -6,7 +6,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SumaTube.Infra.MongoDB.Documents
namespace SumaTube.Infra.MongoDB.Documents.UserPlan
{
public class PersonUserDocument
{

View File

@ -4,7 +4,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SumaTube.Infra.MongoDB.Documents
namespace SumaTube.Infra.MongoDB.Documents.UserPlan
{
public class UserPaymentDocument
{

View File

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SumaTube.Infra.MongoDB.Documents.Videos
{
public class VideoProcessingMessageDocument
{
public string SessionId { get; set; }
public string Url { get; set; }
public string Language { get; set; }
public VideoProcessingMessageDocument(string sessionId, string url, string language)
{
SessionId = sessionId;
Url = url;
Language = language;
}
}
}

View File

@ -1,6 +1,6 @@
using SumaTube.Domain.Entities.UserPlan;
using SumaTube.Infra.MongoDB.Documents;
using SumaTube.Crosscutting.Mappers;
using SumaTube.Infra.MongoDB.Documents.UserPlan;
namespace SumaTube.Infra.MongoDB.Mappers.UserPlan
{

View File

@ -1,7 +1,7 @@
using MongoDB.Driver;
using SumaTube.Infra.MongoDB.Documents;
using SumaTube.Infra.MongoDB.Documents.UserPlan;
namespace SumaTube.Infra.MongoDB.Repositories
namespace SumaTube.Infra.MongoDB.Repositories.UserPlan
{
public class PersonUserRepository
{

View File

@ -0,0 +1,73 @@
using Microsoft.Extensions.Logging;
using MongoDB.Driver;
using SumaTube.Domain.Entities.Videos;
using SumaTube.Infra.Contracts.Repositories.Videos;
namespace SumaTube.Infra.MongoDB.Repositories.Videos
{
public class VideoSummaryRepository : IVideoSummaryRepository
{
private readonly IMongoCollection<VideoSummary> _collection;
private readonly ILogger<VideoSummaryRepository> _logger;
public VideoSummaryRepository(IMongoDatabase database, ILogger<VideoSummaryRepository> logger)
{
_collection = database.GetCollection<VideoSummary>("VideoSummaries");
_logger = logger;
}
public async Task<VideoSummary> GetByIdAsync(string id)
{
_logger.LogInformation("Obtendo resumo por ID: {Id}", id);
return await _collection.Find(x => x.Id == id).FirstOrDefaultAsync();
}
public async Task<List<VideoSummary>> GetByUserIdAsync(string userId)
{
_logger.LogInformation("Obtendo resumos do usuário: {UserId}", userId);
return await _collection.Find(x => x.UserId == userId)
.SortByDescending(x => x.RequestDate)
.ToListAsync();
}
public async Task AddAsync(VideoSummary videoSummary)
{
_logger.LogInformation("Adicionando novo resumo. ID: {Id}, VideoId: {VideoId}",
videoSummary.Id, videoSummary.VideoId);
await _collection.InsertOneAsync(videoSummary);
}
public async Task UpdateAsync(VideoSummary videoSummary)
{
_logger.LogInformation("Atualizando resumo. ID: {Id}, Status: {Status}",
videoSummary.Id, videoSummary.Status);
await _collection.ReplaceOneAsync(x => x.Id == videoSummary.Id, videoSummary);
}
public async Task<bool> ExistsAsync(string videoId, string userId, string language)
{
_logger.LogInformation("Verificando existência de resumo. VideoId: {VideoId}, UserId: {UserId}, Language: {Language}",
videoId, userId, language);
var count = await _collection.CountDocumentsAsync(x =>
x.VideoId == videoId &&
x.UserId == userId &&
x.Language == language &&
(x.Status == "REALIZADO" || x.Status == "PROCESSANDO"));
return count > 0;
}
public async Task<VideoSummary> GetByVideoIdAndUserIdAndLanguageAsync(string videoId, string userId, string language)
{
_logger.LogInformation("Obtendo resumo por VideoId: {VideoId}, UserId: {UserId}, Language: {Language}",
videoId, userId, language);
return await _collection.Find(x =>
x.VideoId == videoId &&
x.UserId == userId &&
x.Language == language)
.FirstOrDefaultAsync();
}
}
}

View File

@ -0,0 +1,39 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using MongoDB.Driver;
using SumaTube.Infra.Contracts.Repositories.Videos;
using SumaTube.Infra.MongoDB.Repositories.Videos;
using SumaTube.Infra.VideoSumarizer.Contracts;
using SumaTube.Infra.VideoSumarizer.Videos;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SumaTube.Infra.Register
{
public static class InfraServicesRegister
{
public static IServiceCollection AddInfraServices(this IServiceCollection services, ConfigurationManager configuration)
{
var mongoConnectionString = configuration.GetConnectionString("MongoDB") ?? "mongodb://localhost:27017";
var mongoDatabaseName = configuration.GetValue<string>("MongoDB:DatabaseName") ?? "SumaTube";
services.AddSingleton<IMongoClient>(sp =>
{
return new MongoClient(mongoConnectionString);
});
services.AddScoped<IMongoDatabase>(sp =>
{
var client = sp.GetRequiredService<IMongoClient>();
return client.GetDatabase(mongoDatabaseName);
});
services.AddScoped<IVideoSumarizerService, VideoSumarizerService>();
services.AddScoped<IVideoSummaryRepository, VideoSummaryRepository>();
return services;
}
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SumaTube.Infra.VideoSumarizer.Contracts
{
public interface IVideoSumarizerService
{
Task RequestVideoSummarization(string sessionId, string url, string language);
}
}

View File

@ -0,0 +1,139 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using RabbitMQ.Client.Events;
using RabbitMQ.Client;
using SumaTube.Infra.Contracts.Repositories.Videos;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static MongoDB.Driver.WriteConcern;
namespace SumaTube.Infra.VideoSumarizer.Videos
{
public class VideoProcessingCompletedListener : BackgroundService
{
private readonly IVideoSummaryRepository _repository;
private readonly ILogger<VideoProcessingCompletedListener> _logger;
private IConnection _connection;
//private IModel _channel;
private static string _hostName = Environment.GetEnvironmentVariable("RABBITMQ_HOST") ?? "localhost";
private static string _userName = Environment.GetEnvironmentVariable("RABBITMQ_USER") ?? "guest";
private static string _password = Environment.GetEnvironmentVariable("RABBITMQ_PASSWORD") ?? "guest";
private static string _queueName = "video-processing-completed";
public VideoProcessingCompletedListener(
IVideoSummaryRepository repository,
ILogger<VideoProcessingCompletedListener> logger)
{
_repository = repository;
_logger = logger;
InitializeRabbitMQ();
}
private void InitializeRabbitMQ()
{
_logger.LogInformation("Inicializando conexão com RabbitMQ em {HostName}...", _hostName);
try
{
//var factory = new ConnectionFactory()
//{
// HostName = _hostName,
// UserName = _userName,
// Password = _password,
// DispatchConsumersAsync = true
//};
//_connection = factory.CreateConnection();
//_channel = _connection.CreateModel();
//_channel.QueueDeclare(
// queue: _queueName,
// durable: true,
// exclusive: false,
// autoDelete: false,
// arguments: null);
_logger.LogInformation("Conexão com RabbitMQ estabelecida. Aguardando mensagens na fila: {QueueName}", _queueName);
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao inicializar conexão com RabbitMQ: {Message}", ex.Message);
throw;
}
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
//stoppingToken.ThrowIfCancellationRequested();
//var consumer = new AsyncEventingBasicConsumer(_channel);
//consumer.Received += async (model, ea) =>
//{
// var body = ea.Body.ToArray();
// var message = Encoding.UTF8.GetString(body);
// _logger.LogInformation("Mensagem recebida: {Message}", message);
// try
// {
// var completedMessage = JsonConvert.DeserializeObject<VideoProcessingCompletedMessage>(message);
// // Buscar o resumo pelo sessionId
// var summaries = await _repository.GetByUserIdAsync(completedMessage.UserId);
// var summary = summaries.Find(s => s.SessionId == completedMessage.SessionId);
// if (summary != null)
// {
// _logger.LogInformation("Atualizando resumo. ID: {Id}, SessionId: {SessionId}",
// summary.Id, completedMessage.SessionId);
// if (completedMessage.Success)
// {
// summary.SetAsCompleted(
// completedMessage.Title,
// completedMessage.Summary,
// completedMessage.Transcription,
// completedMessage.Duration);
// }
// else
// {
// summary.SetAsFailed(completedMessage.ErrorMessage);
// }
// await _repository.UpdateAsync(summary);
// _logger.LogInformation("Resumo atualizado com sucesso. ID: {Id}, Status: {Status}",
// summary.Id, summary.Status);
// }
// else
// {
// _logger.LogWarning("Resumo não encontrado para SessionId: {SessionId}", completedMessage.SessionId);
// }
// _channel.BasicAck(ea.DeliveryTag, false);
// }
// catch (Exception ex)
// {
// _logger.LogError(ex, "Erro ao processar mensagem: {Message}", ex.Message);
// _channel.BasicNack(ea.DeliveryTag, false, true);
// }
//};
//_channel.BasicConsume(queue: _queueName, autoAck: false, consumer: consumer);
return Task.CompletedTask;
}
public override void Dispose()
{
//_channel?.Close();
//_connection?.Close();
base.Dispose();
}
}
}

View File

@ -1,4 +1,6 @@
using RabbitMQ.Client;
using Microsoft.Extensions.Logging;
using RabbitMQ.Client;
using SumaTube.Infra.VideoSumarizer.Contracts;
using System;
using System.Collections.Generic;
using System.Linq;
@ -8,17 +10,24 @@ using System.Threading.Tasks;
namespace SumaTube.Infra.VideoSumarizer.Videos
{
public class VideoSumarizerService
public class VideoSumarizerService : IVideoSumarizerService
{
private static string _hostName = Environment.GetEnvironmentVariable("RABBITMQ_HOST") ?? "localhost";
private static string _userName = Environment.GetEnvironmentVariable("RABBITMQ_USER") ?? "guest";
private static string _password = Environment.GetEnvironmentVariable("RABBITMQ_PASSWORD") ?? "guest";
private static string _queueName = "video-processing-queue";
private readonly ILogger<VideoSumarizerService> _logger;
public VideoSumarizerService(ILogger<VideoSumarizerService> logger)
{
_logger = logger;
}
public async Task RequestVideoSummarization(string sessionId, string url, string language)
{
Console.WriteLine("### Video Processor Publisher ###");
Console.WriteLine($"Conectando ao RabbitMQ em {_hostName}...");
_logger.LogInformation("### Video Processor Publisher ###");
_logger.LogInformation("Conectando ao RabbitMQ em {HostName}...", _hostName);
try
{
@ -44,12 +53,8 @@ namespace SumaTube.Infra.VideoSumarizer.Videos
Persistent = true
};
Console.WriteLine("Conexão estabelecida com RabbitMQ");
Console.WriteLine("Pressione 'Ctrl+C' para sair");
_logger.LogInformation("Conexão estabelecida com RabbitMQ");
// Loop para publicar mensagens
while (true)
{
if (string.IsNullOrWhiteSpace(language))
language = "pt";
@ -64,24 +69,21 @@ namespace SumaTube.Infra.VideoSumarizer.Videos
var body = Encoding.UTF8.GetBytes(messageJson);
// Publicar mensagem
await channel.BasicPublishAsync(exchange: string.Empty,
await channel.BasicPublishAsync(
exchange: string.Empty,
routingKey: _queueName,
mandatory: true,
basicProperties: properties,
body: body);
Console.WriteLine($"[x] Mensagem enviada: {messageJson}");
}
_logger.LogInformation("Mensagem enviada: {Message}", messageJson);
}
}
catch (Exception ex)
{
Console.WriteLine($"Erro: {ex.Message}");
Console.WriteLine(ex.StackTrace);
}
Console.WriteLine("Publicador finalizado.");
Console.ReadLine();
_logger.LogError(ex, "Erro ao enviar mensagem para RabbitMQ: {Message}", ex.Message);
throw;
}
}
}
}

View File

@ -0,0 +1,20 @@
using Microsoft.Extensions.DependencyInjection;
using SumaTube.Application.Videos.ApplicationServices;
using SumaTube.Application.Videos.Contracts;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SumaTube.Application.Register
{
public static class ApplicationServiceRegister
{
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
{
services.AddScoped<IVideoApplicationService, VideoApplicationService>();
return services;
}
}
}

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\BaseDomain\BaseDomain.csproj" />
<ProjectReference Include="..\SumaTuba.Infra\SumaTube.Infra.csproj" />
<ProjectReference Include="..\SumaTube.Crosscutting\SumaTube.Crosscutting.csproj" />
<ProjectReference Include="..\SumaTube.Domain\SumaTube.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="UserPlan\Contracts\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,23 @@
using SumaTube.Domain.Entities.UserPlan;
using SumaTube.Infra.Contracts.Repositories.UserPlan;
using SumaTube.Infra.MongoDB.Mappers.UserPlan;
namespace SumaTube.Application.UserPlan.ApplicationServices
{
public class PersonUserAppService
{
private readonly IPersonUserRepository _personUserRepository;
public PersonUserAppService(IPersonUserRepository personUserRepository)
{
_personUserRepository = personUserRepository;
}
public async Task<PersonUser> GetPersonUserByEmailAsync(string email)
{
var document = await _personUserRepository.GetByEmailAsync(email);
return document.ToDomain();
}
}
}

View File

@ -0,0 +1,137 @@
using Microsoft.Extensions.Logging;
using SumaTube.Application.Videos.Contracts;
using SumaTube.Domain.Entities.Videos;
using SumaTube.Infra.Contracts.Repositories.Videos;
using SumaTube.Infra.VideoSumarizer.Contracts;
using SumaTube.Infra.VideoSumarizer.Videos;
namespace SumaTube.Application.Videos.ApplicationServices
{
public class VideoApplicationService : IVideoApplicationService
{
private readonly IVideoSummaryRepository _repository;
private readonly IVideoSumarizerService _sumarizerService;
private readonly ILogger<VideoApplicationService> _logger;
public VideoApplicationService(
IVideoSummaryRepository repository,
IVideoSumarizerService sumarizerService,
ILogger<VideoApplicationService> logger)
{
_repository = repository;
_sumarizerService = sumarizerService;
_logger = logger;
}
public async Task<List<VideoSummary>> GetUserVideosAsync(string userId)
{
_logger.LogInformation("Obtendo videos do usuário: {UserId}", userId);
return await _repository.GetByUserIdAsync(userId);
}
public async Task<VideoSummary> GetVideoSummaryByIdAsync(string id, string userId)
{
_logger.LogInformation("Obtendo resumo com ID: {SummaryId} para usuário: {UserId}", id, userId);
var summary = await _repository.GetByIdAsync(id);
if (summary == null || summary.UserId != userId)
{
_logger.LogWarning("Resumo não encontrado ou acesso não autorizado. ID: {SummaryId}, UserId: {UserId}", id, userId);
return null;
}
return summary;
}
public async Task<VideoSummary> RequestVideoSummaryAsync(string youtubeUrl, string language, string userId)
{
_logger.LogInformation("Solicitando resumo para URL: {Url}, idioma: {Language}, usuário: {UserId}", youtubeUrl, language, userId);
try
{
// Extrair ID do vídeo
string videoId = ExtractVideoId(youtubeUrl);
if (string.IsNullOrEmpty(videoId))
{
_logger.LogWarning("URL do YouTube inválida: {Url}", youtubeUrl);
throw new ArgumentException("URL do YouTube inválida");
}
// Verificar se já existe um resumo para este vídeo/usuário/idioma
bool exists = await _repository.ExistsAsync(videoId, userId, language);
if (exists)
{
_logger.LogInformation("Resumo já existente para vídeo: {VideoId}, usuário: {UserId}, idioma: {Language}",
videoId, userId, language);
return await _repository.GetByVideoIdAndUserIdAndLanguageAsync(videoId, userId, language);
}
// Criar novo resumo
var sessionId = Guid.NewGuid().ToString();
var summary = VideoSummary.Create(videoId, userId, language, sessionId);
// Salvar no repositório
await _repository.AddAsync(summary);
// Enviar para processamento via RabbitMQ
_logger.LogInformation("Enviando solicitação para processamento. SessionId: {SessionId}", sessionId);
await _sumarizerService.RequestVideoSummarization(sessionId, youtubeUrl, language);
return summary;
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao solicitar resumo de vídeo: {Message}", ex.Message);
throw;
}
}
public async Task<object> CheckSummaryStatusAsync(string id, string userId)
{
_logger.LogInformation("Verificando status do resumo. ID: {SummaryId}, usuário: {UserId}", id, userId);
var summary = await _repository.GetByIdAsync(id);
if (summary == null || summary.UserId != userId)
{
_logger.LogWarning("Resumo não encontrado ou acesso não autorizado. ID: {SummaryId}, UserId: {UserId}", id, userId);
return new { status = "NOT_FOUND" };
}
return new
{
status = summary.Status,
title = summary.Title,
thumbnailUrl = summary.ThumbnailUrl,
errorMessage = summary.ErrorMessage
};
}
private string ExtractVideoId(string url)
{
try
{
var uri = new Uri(url);
if (uri.Host.Contains("youtube.com"))
{
var query = System.Web.HttpUtility.ParseQueryString(uri.Query);
return query["v"];
}
else if (uri.Host.Contains("youtu.be"))
{
var segments = uri.Segments;
return segments[segments.Length - 1].TrimEnd('/');
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao extrair ID do vídeo da URL: {Url}", url);
}
return null;
}
}
}

View File

@ -0,0 +1,17 @@
using SumaTube.Domain.Entities.Videos;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SumaTube.Application.Videos.Contracts
{
public interface IVideoApplicationService
{
Task<List<VideoSummary>> GetUserVideosAsync(string userId);
Task<VideoSummary> GetVideoSummaryByIdAsync(string id, string userId);
Task<VideoSummary> RequestVideoSummaryAsync(string youtubeUrl, string language, string userId);
Task<object> CheckSummaryStatusAsync(string id, string userId);
}
}

View File

@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SumaTube.Domain.Entities.Videos
{
public class VideoSummary
{
public string Id { get; private set; }
public string VideoId { get; private set; }
public string Title { get; private set; }
public string ThumbnailUrl { get; private set; }
public string Status { get; private set; }
public DateTime RequestDate { get; private set; }
public string Language { get; private set; }
public string UserId { get; private set; }
public string Summary { get; private set; }
public string Transcription { get; private set; }
public int Duration { get; private set; }
public string ErrorMessage { get; private set; }
public string SessionId { get; private set; }
// Factory method
public static VideoSummary Create(string videoId, string userId, string language, string sessionId)
{
return new VideoSummary
{
Id = Guid.NewGuid().ToString(),
VideoId = videoId,
Title = "Carregando...",
ThumbnailUrl = $"https://img.youtube.com/vi/{videoId}/maxresdefault.jpg",
Status = "PROCESSANDO",
RequestDate = DateTime.UtcNow,
Language = language,
UserId = userId,
SessionId = sessionId
};
}
// Methods to update state
public void SetAsCompleted(string title, string summary, string transcription, int duration)
{
Title = title;
Summary = summary;
Transcription = transcription;
Duration = duration;
Status = "REALIZADO";
ErrorMessage = null;
}
public void SetAsFailed(string errorMessage)
{
Status = "ERRO";
ErrorMessage = errorMessage;
}
// For MongoDB/ORM
private VideoSummary() { }
}
}

View File

@ -13,7 +13,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseDomain", "BaseDomain\Ba
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SumaTube.Crosscutting", "SumaTube.Crosscutting\SumaTube.Crosscutting.csproj", "{46EE417D-A974-4011-9799-646F31C9C146}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SumaTube.Application", "..\SumaTube.Application\SumaTube.Application.csproj", "{C598FDC6-26F5-44B2-AB7C-A5471DBE4168}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SumaTube.Application", "SumaTube.Application\SumaTube.Application.csproj", "{142F15AA-CD0E-CC29-6972-3FE9A1DB283F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -41,10 +41,10 @@ Global
{46EE417D-A974-4011-9799-646F31C9C146}.Debug|Any CPU.Build.0 = Debug|Any CPU
{46EE417D-A974-4011-9799-646F31C9C146}.Release|Any CPU.ActiveCfg = Release|Any CPU
{46EE417D-A974-4011-9799-646F31C9C146}.Release|Any CPU.Build.0 = Release|Any CPU
{C598FDC6-26F5-44B2-AB7C-A5471DBE4168}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C598FDC6-26F5-44B2-AB7C-A5471DBE4168}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C598FDC6-26F5-44B2-AB7C-A5471DBE4168}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C598FDC6-26F5-44B2-AB7C-A5471DBE4168}.Release|Any CPU.Build.0 = Release|Any CPU
{142F15AA-CD0E-CC29-6972-3FE9A1DB283F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{142F15AA-CD0E-CC29-6972-3FE9A1DB283F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{142F15AA-CD0E-CC29-6972-3FE9A1DB283F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{142F15AA-CD0E-CC29-6972-3FE9A1DB283F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -0,0 +1,114 @@
using Microsoft.AspNetCore.Mvc;
using SumaTube.Application.Videos.ApplicationServices;
using SumaTube.Application.Videos.Contracts;
using SumaTube.Domain.Entities.Videos;
using SumaTube.Models;
using System.Collections.Generic;
namespace SumaTube.Controllers
{
public class VideoController : Controller
{
private readonly IVideoApplicationService _videoApplicationService;
private readonly ILogger<VideoController> _logger;
public VideoController(
IVideoApplicationService videoApplicationService,
ILogger<VideoController> logger)
{
_videoApplicationService = videoApplicationService;
_logger = logger;
}
public IActionResult Extract()
{
return View();
}
public async Task<IActionResult> VideoSummary(string id)
{
// Aqui você chamaria sua WebAPI para processar o vídeo
// e retornaria o modelo para a view
var model = new VideoSummaryViewModel
{
VideoId = id,
VideoTitle = "Título do Vídeo",
ChannelName = "Nome do Canal",
ChannelThumbnail = "URL da Thumbnail do Canal",
PublishedDate = "Data de Publicação",
ViewCount = "Quantidade de Visualizações",
LikeCount = "Quantidade de Curtidas",
CommentCount = "Quantidade de Comentarios",
SummaryText = "Resumo do Vídeo",
KeyPoints = new List<object> { "Ponto 1", "Ponto 2", "Ponto 3" },
Captions = new List<object> { "Legenda 1", "Legenda 2", "Legenda 3" },
Keywords = new List<string> { "Palavra 1", "Palavra 2", "Palavra 3" },
RelatedTopics = new List<string> { "Tópico 1", "Tópico 2", "Tópico 3" },
RelatedVideos = new List<object> { "Vídeo 1", "Vídeo 2", "Vídeo 3" }
};
return View(model);
}
public async Task<IActionResult> MySummary()
{
var userId = User.Identity.Name; // Ou outra forma de obter o ID do usuário
_logger.LogInformation("Carregando página 'Meus Resumos' para usuário: {UserId}", userId);
var videos = new List<VideoSummary>();
if (userId != null)
videos = await _videoApplicationService.GetUserVideosAsync(userId);
return View(videos);
}
// Endpoint para enviar solicitação de novo resumo
[HttpPost]
public async Task<IActionResult> RequestSummary(string youtubeUrl, string language)
{
var userId = User.Identity.Name; // Ou outra forma de obter o ID do usuário
_logger.LogInformation("Recebido pedido de resumo. URL: {Url}, Idioma: {Language}, Usuário: {UserId}",
youtubeUrl, language, userId);
try
{
var result = await _videoApplicationService.RequestVideoSummaryAsync(youtubeUrl, language, userId);
_logger.LogInformation("Resumo solicitado com sucesso. ID: {Id}", result.Id);
return Json(new { success = true, videoId = result.VideoId, summaryId = result.Id });
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao solicitar resumo: {Message}", ex.Message);
return Json(new { success = false, error = ex.Message });
}
}
// Página para exibir um resumo específico
public async Task<IActionResult> Summary(string id)
{
var userId = User.Identity.Name; // Ou outra forma de obter o ID do usuário
_logger.LogInformation("Acessando resumo. ID: {Id}, Usuário: {UserId}", id, userId);
var summary = await _videoApplicationService.GetVideoSummaryByIdAsync(id, userId);
if (summary == null)
{
_logger.LogWarning("Resumo não encontrado. ID: {Id}", id);
return NotFound();
}
return View(summary);
}
// Endpoint para verificar status de processamento
[HttpGet]
public async Task<IActionResult> CheckStatus(string id)
{
var userId = User.Identity.Name; // Ou outra forma de obter o ID do usuário
_logger.LogInformation("Verificando status do resumo. ID: {Id}, Usuário: {UserId}", id, userId);
var status = await _videoApplicationService.CheckSummaryStatusAsync(id, userId);
return Json(status);
}
}
}

View File

@ -0,0 +1,23 @@
using System;
namespace SumaTube.Models
{
public class VideoSummaryItem
{
public string Id { get; set; }
public string VideoId { get; set; } // ID do vídeo do YouTube
public string Title { get; set; }
public string ThumbnailUrl { get; set; }
public string Status { get; set; } // PROCESSANDO, REALIZADO, ERRO
public DateTime RequestDate { get; set; }
public string Language { get; set; }
public string UserId { get; set; } // ID do usuário que solicitou o resumo
// Propriedades para quando o resumo for concluído
public string Summary { get; set; }
public string Transcription { get; set; }
public int Duration { get; set; } // Duração do vídeo em segundos
public string ErrorMessage { get; set; }
}
}

View File

@ -0,0 +1,20 @@
namespace SumaTube.Models
{
public class VideoSummaryViewModel
{
public string VideoId { get; set; }
public string VideoTitle { get; set; }
public string ChannelName { get; set; }
public string ChannelThumbnail { get; set; }
public string PublishedDate { get; set; }
public string ViewCount { get; set; }
public string LikeCount { get; set; }
public string CommentCount { get; set; }
public string SummaryText { get; set; }
public List<object> KeyPoints { get; set; }
public List<object> Captions { get; set; }
public List<string> Keywords { get; set; }
public List<string> RelatedTopics { get; set; }
public List<object> RelatedVideos { get; set; }
}
}

View File

@ -12,6 +12,8 @@ using Stripe.Forwarding;
using System.Globalization;
using System.Security.Policy;
using SumaTube.Crosscutting.Logging.Configuration;
using SumaTube.Application.Register;
using SumaTube.Infra.Register;
var builder = WebApplication.CreateBuilder(args);
@ -23,6 +25,9 @@ builder.Host.UseSerilog((context, services, configuration) =>
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddApplicationServices();
builder.Services.AddInfraServices(builder.Configuration);
var config = builder.Configuration;
//builder.Services.AddAuthentication()

View File

@ -9,6 +9,13 @@
<DockerfileContext>.</DockerfileContext>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Views\CartHome\**" />
<Content Remove="Views\CartHome\**" />
<EmbeddedResource Remove="Views\CartHome\**" />
<None Remove="Views\CartHome\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="8.0.7" />
@ -26,11 +33,11 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Views\CartHome\" />
<Folder Include="wwwroot\img\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SumaTube.Application\SumaTube.Application.csproj" />
<ProjectReference Include="..\SumaTube.Crosscutting\SumaTube.Crosscutting.csproj" />
</ItemGroup>

View File

@ -1,79 +1,279 @@
@{
ViewData["Title"] = "Home Page";
ViewBag.Title = "Resumos Inteligentes de Vídeos";
}
@section Scripts {
<script type="text/javascript">
</script>
}
<!-- Hero Banner -->
<div class="hero-banner mb-5">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-6">
<h1>Transforme qualquer vídeo em um resumo inteligente</h1>
<p class="lead">Extraímos as legendas e criamos resumos precisos para você economizar tempo e maximizar o aprendizado.</p>
@section Styles {
<style type="text/css">
.text-responsive {
font-size: calc(100% + 1vw + 1vh);
}
</style>
}
<div class="div-body">
<div class="container">
<div id="myCarousel" class="carousel slide" data-ride="carousel">
<ol class="carousel-indicators">
<li data-target="#myCarousel" data-slide-to="0" class="active"></li>
<li data-target="#myCarousel" data-slide-to="1"></li>
<li data-target="#myCarousel" data-slide-to="2"></li>
<li data-target="#myCarousel" data-slide-to="3"></li>
</ol>
<div class="carousel-inner" role="listbox">
<div class="carousel-item active">
<img class="first-slide" src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" alt="First slide">
<div class="container">
<div class="carousel-caption d-md-block text-left">
<h1 class="text-responsive">Example headline.</h1>
<p>Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec id elit non mi porta gravida at eget metus. Nullam id dolor id nibh ultricies vehicula ut id elit.</p>
<p><a class="btn btn-lg btn-primary" href="#" role="button">Sign up today</a></p>
</div>
</div>
</div>
<div class="carousel-item">
<img class="second-slide" src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" alt="Second slide">
<div class="container">
<div class="carousel-caption d-md-block">
<h1 class="text-responsive">Another example headline.</h1>
<p>Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec id elit non mi porta gravida at eget metus. Nullam id dolor id nibh ultricies vehicula ut id elit.</p>
<p><a class="btn btn-lg btn-primary" href="#" role="button">Learn more</a></p>
</div>
</div>
</div>
<div class="carousel-item">
<img class="third-slide" src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" alt="Third slide">
<div class="container">
<div class="carousel-caption d-md-block text-right">
<h1 class="text-responsive">One more for good measure.</h1>
<p>Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec id elit non mi porta gravida at eget metus. Nullam id dolor id nibh ultricies vehicula ut id elit.</p>
<p><a class="btn btn-lg btn-primary" href="#" role="button">Browse gallery</a></p>
</div>
</div>
</div>
<div class="carousel-item">
<img class="fourth-slide" src="/img/teste1.png" alt="Fourth slide">
<div class="container">
<div class="carousel-caption d-md-block text-right text-dark">
<h1 class="text-responsive">One more for good measure.</h1>
<p>Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec id elit non mi porta gravida at eget metus. Nullam id dolor id nibh ultricies vehicula ut id elit.</p>
<p><a class="btn btn-lg btn-primary" href="#" role="button">Browse gallery</a></p>
</div>
</div>
</div>
</div>
<a class="carousel-control-prev" href="#myCarousel" role="button" data-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="sr-only">Anterior</span>
<a href="@Url.Action("Index", "Login")" class="btn btn-hero">
<i class="bi bi-magic mr-2"></i> Fazer login e resumir um vídeo
</a>
<a class="carousel-control-next" href="#myCarousel" role="button" data-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="sr-only">Próximo</span>
</div>
<div class="col-lg-6 mt-4 mt-lg-0 text-center">
<div class="hero-image">
<img src="/img/video-summary-demo.png" alt="Ilustração de resumo de vídeo" class="img-fluid" onerror="this.src='https://via.placeholder.com/600x350?text=SumaTube';" />
<span class="demo-badge">
<i class="bi bi-play-circle mr-1"></i> Ver demonstração
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Como funciona -->
<section class="mb-5">
<div class="container">
<h2 class="text-center mb-4">Como funciona</h2>
<div class="row">
<div class="col-md-4 mb-4">
<div class="suma-card h-100 text-center">
<div class="mb-3">
<i class="bi bi-youtube text-danger" style="font-size: 3rem;"></i>
</div>
<h3 class="suma-card-title">1. Cole o link do YouTube</h3>
<p class="suma-card-body">
Basta colar o URL do vídeo do YouTube que você deseja resumir. Funciona com qualquer vídeo que tenha legendas disponíveis.
</p>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="suma-card h-100 text-center">
<div class="mb-3">
<i class="bi bi-cpu text-danger" style="font-size: 3rem;"></i>
</div>
<h3 class="suma-card-title">2. Processamento inteligente</h3>
<p class="suma-card-body">
Nossa tecnologia extrai as legendas e utiliza inteligência artificial para identificar os pontos mais importantes do vídeo.
</p>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="suma-card h-100 text-center">
<div class="mb-3">
<i class="bi bi-file-earmark-text text-danger" style="font-size: 3rem;"></i>
</div>
<h3 class="suma-card-title">3. Receba seu resumo</h3>
<p class="suma-card-body">
Em poucos segundos, você recebe um resumo completo e organizado com os principais pontos abordados no vídeo.
</p>
</div>
</div>
</div>
</div>
</section>
<!-- Recursos -->
<section class="mb-5 bg-light py-5">
<div class="container">
<h2 class="text-center mb-4">Economize tempo com nossos recursos</h2>
<div class="row align-items-center">
<div class="col-lg-6 mb-4 mb-lg-0 order-lg-2">
@*
<img src="/images/features-illustration.png" alt="Recursos do SumaTube" class="img-fluid rounded shadow" onerror="this.src='https://via.placeholder.com/600x400?text=Recursos+SumaTube';" />
*@
</div>
<div class="col-lg-6 order-lg-1">
<div class="d-flex mb-4">
<div class="mr-3">
<i class="bi bi-clock text-danger" style="font-size: 2rem;"></i>
</div>
<div>
<h3>Economize tempo</h3>
<p>Assista apenas o que realmente importa com nossos resumos personalizados.</p>
</div>
</div>
<div class="d-flex mb-4">
<div class="mr-3">
<i class="bi bi-translate text-danger" style="font-size: 2rem;"></i>
</div>
<div>
<h3>Suporte multi-idioma</h3>
<p>Resuma vídeos em diversos idiomas com a mesma precisão e qualidade.</p>
</div>
</div>
<div class="d-flex mb-4">
<div class="mr-3">
<i class="bi bi-bookmark-check text-danger" style="font-size: 2rem;"></i>
</div>
<div>
<h3>Pontos-chave destacados</h3>
<p>Identificamos automaticamente os conceitos e informações mais importantes em cada vídeo.</p>
</div>
</div>
<div class="d-flex">
<div class="mr-3">
<i class="bi bi-cloud-download text-danger" style="font-size: 2rem;"></i>
</div>
<div>
<h3>Salve para consultar depois</h3>
<p>Mantenha todos seus resumos organizados em sua biblioteca pessoal.</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Planos -->
<section class="mb-5">
<div class="container">
<div class="text-center mb-5">
<h2>Escolha o plano ideal para você</h2>
<p class="lead">Oferecemos opções flexíveis para atender às suas necessidades</p>
</div>
<div class="row">
<div class="col-md-4 mb-4">
<div class="suma-card h-100 text-center">
<span class="badge badge-pill badge-light mb-3">Gratuito</span>
<h3 class="suma-card-title">Basic</h3>
<div class="my-4">
<span class="h1">R$ 0</span>
<span class="text-muted">/mês</span>
</div>
<ul class="list-unstyled mb-4">
<li class="mb-2">3 resumos por mês</li>
<li class="mb-2">Resumos básicos</li>
<li class="mb-2">Suporte por e-mail</li>
<li class="mb-2 text-muted"><del>Biblioteca personalizada</del></li>
<li class="mb-2 text-muted"><del>Exportação em PDF</del></li>
</ul>
<a href="#" class="btn btn-outline-danger">Começar agora</a>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="suma-card h-100 text-center" style="transform: scale(1.05); border: 2px solid var(--suma-red);">
<span class="badge badge-pill badge-danger mb-3">Popular</span>
<h3 class="suma-card-title">Pro</h3>
<div class="my-4">
<span class="h1">R$ 19,90</span>
<span class="text-muted">/mês</span>
</div>
<ul class="list-unstyled mb-4">
<li class="mb-2">30 resumos por mês</li>
<li class="mb-2">Resumos detalhados</li>
<li class="mb-2">Suporte prioritário</li>
<li class="mb-2">Biblioteca personalizada</li>
<li class="mb-2">Exportação em PDF</li>
</ul>
<a href="#" class="btn btn-danger">Escolher Pro</a>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="suma-card h-100 text-center">
<span class="badge badge-pill badge-light mb-3">Premium</span>
<h3 class="suma-card-title">Business</h3>
<div class="my-4">
<span class="h1">R$ 49,90</span>
<span class="text-muted">/mês</span>
</div>
<ul class="list-unstyled mb-4">
<li class="mb-2">Resumos ilimitados</li>
<li class="mb-2">Resumos avançados</li>
<li class="mb-2">Suporte VIP</li>
<li class="mb-2">Biblioteca com categorias</li>
<li class="mb-2">Exportação em vários formatos</li>
</ul>
<a href="#" class="btn btn-outline-danger">Escolher Business</a>
</div>
</div>
</div>
</div>
</section>
<!-- Depoimentos -->
<section class="mb-5 bg-light py-5">
<div class="container">
<h2 class="text-center mb-5">O que nossos usuários dizem</h2>
<div class="row">
<div class="col-md-4 mb-4">
<div class="suma-card h-100">
<div class="d-flex mb-3 align-items-center">
<div class="rounded-circle overflow-hidden mr-3" style="width: 50px; height: 50px;">
<img src="https://via.placeholder.com/50x50" alt="Usuário" class="img-fluid" />
</div>
<div>
<h5 class="mb-0">Mariana Silva</h5>
<small class="text-muted">Estudante de Medicina</small>
</div>
</div>
<p class="suma-card-body">
"O SumaTube revolucionou meus estudos. Consigo extrair o essencial de aulas longas em minutos, o que me ajudou a otimizar muito meu tempo de estudo."
</p>
<div class="text-warning">
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="suma-card h-100">
<div class="d-flex mb-3 align-items-center">
<div class="rounded-circle overflow-hidden mr-3" style="width: 50px; height: 50px;">
<img src="https://via.placeholder.com/50x50" alt="Usuário" class="img-fluid" />
</div>
<div>
<h5 class="mb-0">Carlos Mendes</h5>
<small class="text-muted">Profissional de Marketing</small>
</div>
</div>
<p class="suma-card-body">
"Uso o SumaTube para acompanhar webinars e palestras do meu setor. Os resumos são precisos e me ajudam a extrair insights valiosos sem gastar horas assistindo vídeos."
</p>
<div class="text-warning">
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-half"></i>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="suma-card h-100">
<div class="d-flex mb-3 align-items-center">
<div class="rounded-circle overflow-hidden mr-3" style="width: 50px; height: 50px;">
<img src="https://via.placeholder.com/50x50" alt="Usuário" class="img-fluid" />
</div>
<div>
<h5 class="mb-0">Júlia Santos</h5>
<small class="text-muted">Professora</small>
</div>
</div>
<p class="suma-card-body">
"Recomendo o SumaTube para todos meus alunos. É uma ferramenta incrível para complementar os estudos e revisar conteúdos de forma eficiente."
</p>
<div class="text-warning">
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
<i class="bi bi-star-fill"></i>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="mb-5">
<div class="container">
<div class="suma-card bg-danger text-white text-center py-5">
<h2 class="mb-4">Comece a economizar tempo hoje mesmo</h2>
<p class="lead mb-4">Transforme a maneira como você assiste vídeos e absorve conhecimento</p>
<a href="@Url.Action("Index", "Login")" class="btn btn-light btn-lg px-4">
<i class="bi bi-play-circle mr-2"></i> Começar gratuitamente
</a>
</div>
</div>
</div>
</section>

View File

@ -1,102 +0,0 @@
.btn-primary {
background-color: darkgreen;
color: white;
}
.btn-red {
background-color: darkred;
color: white;
}
.div-body {
padding-top: 3rem;
padding-bottom: 3rem;
color: #5a5a5a;
}
.bg-inverse {
background-color: #292b2c !important;
}
/* CUSTOMIZE THE CAROUSEL
-------------------------------------------------- */
/* Carousel base class */
.carousel {
margin-bottom: 4rem;
}
/* Since positioning the image, we need to help out the caption */
.carousel-caption {
z-index: 10;
bottom: 3rem;
}
/* Declare heights because of positioning of img element */
.carousel-item {
height: 32rem;
background-color: #777;
}
.carousel-item > img {
position: absolute;
top: 0;
left: 0;
min-width: 100%;
height: 32rem;
}
/* MARKETING CONTENT
-------------------------------------------------- */
/* Center align the text within the three columns below the carousel */
.marketing .col-lg-4 {
margin-bottom: 1.5rem;
text-align: center;
}
.marketing h2 {
font-weight: normal;
}
.marketing .col-lg-4 p {
margin-right: .75rem;
margin-left: .75rem;
}
/* Featurettes
------------------------- */
.featurette-divider {
margin: 5rem 0; /* Space out the Bootstrap <hr> more */
}
/* Thin out the marketing headings */
.featurette-heading {
font-weight: 300;
line-height: 1;
letter-spacing: -.05rem;
}
/* RESPONSIVE CSS
-------------------------------------------------- */
@media (min-width: 40em) {
/* Bump up size of carousel content */
.carousel-caption p {
margin-bottom: 1.25rem;
font-size: 1.25rem;
line-height: 1.4;
}
.featurette-heading {
font-size: 50px;
}
}
@media (min-width: 62em) {
.featurette-heading {
margin-top: 7rem;
}
}

View File

@ -1,127 +1,111 @@
@{
ViewBag.Title = "Login / Registro";
}
@section Styles {
<style>
.box {
width: 500px;
margin: 200px 0;
.login-container {
max-width: 500px;
margin: 60px auto;
background-color: white;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
padding: 30px;
text-align: center;
}
.shape1 {
position: relative;
height: 150px;
width: 150px;
background-color: #0074d9;
border-radius: 80px;
float: left;
margin-right: -50px;
.login-icon {
font-size: 2.5rem;
color: var(--suma-red);
margin-bottom: 25px;
}
.shape2 {
position: relative;
height: 150px;
width: 150px;
background-color: #0074d9;
border-radius: 80px;
margin-top: -30px;
float: left;
.login-title {
font-size: 28px;
font-weight: 700;
color: var(--suma-red);
margin-bottom: 15px;
}
.shape3 {
position: relative;
height: 150px;
width: 150px;
background-color: #0074d9;
border-radius: 80px;
margin-top: -30px;
float: left;
margin-left: -31px;
.login-subtitle {
font-size: 16px;
color: var(--suma-dark-gray);
margin-bottom: 30px;
}
.shape4 {
position: relative;
height: 150px;
width: 150px;
background-color: #0074d9;
border-radius: 80px;
margin-top: -25px;
float: left;
margin-left: -32px;
.login-button {
display: flex;
align-items: center;
justify-content: center;
background-color: white;
color: #444;
border: 1px solid #ddd;
border-radius: 5px;
padding: 12px 20px;
font-size: 16px;
font-weight: 500;
width: 100%;
transition: all 0.3s ease;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
cursor: pointer;
}
.shape5 {
position: relative;
height: 150px;
width: 150px;
background-color: #0074d9;
border-radius: 80px;
float: left;
margin-right: -48px;
margin-left: -32px;
margin-top: -30px;
.login-button:hover {
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.shape6 {
position: relative;
height: 150px;
width: 150px;
background-color: #0074d9;
border-radius: 80px;
float: left;
margin-right: -20px;
margin-top: -35px;
.login-button i {
margin-right: 10px;
color: #4285F4;
}
.shape7 {
position: relative;
height: 150px;
width: 150px;
background-color: #0074d9;
border-radius: 80px;
float: left;
margin-right: -20px;
margin-top: -57px;
.login-footer {
margin-top: 30px;
font-size: 14px;
color: var(--suma-dark-gray);
}
.float {
position: absolute;
z-index: 2;
.login-footer a {
color: var(--suma-red);
text-decoration: none;
}
.form {
margin-left: 145px;
.login-footer a:hover {
text-decoration: underline;
}
.video-icon-container {
font-size: 4rem;
margin-bottom: 20px;
color: var(--suma-red);
}
.video-icon-container i {
filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.1));
}
</style>
}
<div id="login-row" class="row justify-content-center align-items-center">
<div id="login-column" class="col-md-6">
<div class="box">
<div class="shape1"></div>
<div class="shape2"></div>
<div class="shape3"></div>
<div class="shape4"></div>
<div class="shape5"></div>
<div class="shape6"></div>
<div class="shape7"></div>
<div class="float">
<br />
<br />
@using (Html.BeginForm("ExternalLogin", "Login", new { provider = "Microsoft" }, FormMethod.Post, true, new { id = "externalLoginForm", style = "margin-left: 150px" }))
<div class="login-container">
<div class="login-icon">
<i class="bi bi-play-btn-fill"></i>
</div>
<h1 class="login-title">Bem-vindo ao SumaTube</h1>
<p class="login-subtitle">Faça login para criar resumos inteligentes de vídeos do YouTube</p>
<div class="video-icon-container">
<i class="bi bi-file-earmark-text"></i>
<i class="bi bi-arrow-left-right mx-2"></i>
<i class="bi bi-youtube"></i>
</div>
@using (Html.BeginForm("ExternalLoginGoogle", "Login", new { provider = "Google" }, FormMethod.Post, true, new { id = "googleLoginForm" }))
{
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-success btn-block login">Login with Microsoft</button>
}
<br />
@using (Html.BeginForm("ExternalLoginGoogle", "Login", new { provider = "Google" }, FormMethod.Post, true, new { id = "externalLoginForm", style = "margin-left: 150px" }))
{
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-success btn-block login">Login with Google</button>
<button type="submit" class="login-button">
<i class="bi bi-google"></i>
Continuar/Entrar com Google
</button>
}
</div>
</div>
<div class="login-footer">
Ao fazer login, você concorda com nossos <a href="/termos">Termos de Serviço</a> e <a href="/privacidade">Política de Privacidade</a>
</div>
</div>

View File

@ -0,0 +1,109 @@
@{
ViewBag.Title = "Planos";
}
<div class="row db-padding-btm db-attached">
<div class="col-4 col-xs-4 col-sm-4 col-md-4 col-lg-4">
<div class="db-wrapper">
<div class="db-pricing-eleven db-bk-color-one">
<div class="price text-price-responsive">
<sup>R$</sup> 0,00
<small>para sempre!</small>
</div>
<div class="type text-type-responsive">
Básico
</div>
<ul class="text-responsive">
<li><i class="bi bi-plus-square-fill text-success"></i>1 Bio/Links <span class="hide-in-cell">pessoal virtual</span></li>
<li><i class="bi bi-plus-square-fill text-success"></i><span class="hide-in-cell">Exibir imagem </span>Whatsapp *</li>
<li><i class="bi bi-plus-square-fill text-success"></i>Até 3 links </li>
<li><i class="bi bi-square text-success"></i><span class="hide-in-cell">Sem página de</span> Produtos</li>
<li><i class="bi bi-square text-success"></i><span class="hide-in-cell">Sem contador de </span>Visualizações</span></li>
</ul>
<div class="pricing-footer">
@if (User.Identity.IsAuthenticated)
{
<div class="btn db-button-color-square btn-lg">Plano Inicial</div>
}
else
{
<div class="btn db-button-color-square btn-lg">Comece com este!</div>
}
</div>
</div>
</div>
</div>
<div class="col-4 col-xs-4 col-sm-4 col-md-4 col-lg-4">
<div class="db-wrapper">
<div class="db-pricing-eleven db-bk-color-two popular">
<div class="price text-price-responsive">
<sup>R$</sup>13
<small>por mês</small>
</div>
<div class="type text-type-responsive">
BIO
</div>
<ul>
<li><i class="bi bi-plus-square-fill text-success"></i>1 Bio/Links <span class="hide-in-cell">pessoal virtual</span></li>
<li><i class="bi bi-plus-square-fill text-success"></i><span class="hide-in-cell">Exibir cartão </span>Whatsapp *</li>
<li><i class="bi bi-plus-square-fill text-success"></i>100 links</li>
<li><i class="bi bi-square text-success"></i><span class="hide-in-cell">Sem página de</span> Produtos</li>
@*
<li><i class="bi bi-plus-square-fill text-success"></i>1 Link agendado **</li>
*@
<li><i class="bi bi-square text-success"></i><span class="hide-in-cell">Contador de</span> Visualizações</li>
@*
<li><i class="bi bi-square text-success"></i><span class="hide-in-cell">Registro de contatos</span> </li>
*@
</ul>
<div class="pricing-footer">
@if (User.Identity.IsAuthenticated)
{
<a href="/Pay/?plan=bio" class="btn db-button-color-square btn-lg">Assinar</a>
}
else
{
<a href="/Login" class="btn db-button-color-square btn-lg">Login / Registro</a>
}
</div>
</div>
</div>
</div>
<div class="col-4 col-xs-4 col-sm-4 col-md-4 col-lg-4">
<div class="db-wrapper">
<div class="db-pricing-eleven db-bk-color-three">
<div class="price text-price-responsive">
<sup>R$</sup>27
<small>por mês</small>
</div>
<div class="type text-type-responsive">
CATÁLOGO
</div>
<ul>
<li><i class="bi bi-plus-square-fill text-success"></i>1 Bio/Cartão <span class="hide-in-cell">pessoal virtual</span></li>
<li><i class="bi bi-plus-square-fill text-success"></i><span class="hide-in-cell">Exibir cartão </span>Whatsapp *</li>
<li><i class="bi bi-plus-square-fill text-success"></i>Sem limite de links </li>
<li><i class="bi bi-plus-square-fill text-success"></i>Links de Produtos</li>
<li><i class="bi bi-plus-square-fill text-success"></i>Contador<span class="hide-in-cell"> de visualizações</span> </li>
@*
<li><i class="bi bi-plus-square-fill text-success"></i><span class="hide-in-cell">Registro de contatos</span> </li>
*@
</ul>
<div class="pricing-footer">
@if (User.Identity.IsAuthenticated)
{
<button type="button" class="btn db-button-color-square btn-lg" onclick="waitingDialog.show('Custom message');window.location.href = '/Pay/?plan=catalogo';">Assinar</button>
@*
<a href="/Pay/?plan=catalogo" class="btn db-button-color-square btn-lg">Assinar</a>
*@
}
else
{
<a href="/Login" class="btn db-button-color-square btn-lg">Login / Registro</a>
}
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,225 @@
/*=============================================================
Authour URL: www.designbootstrap.com
http://www.designbootstrap.com/
License: MIT
======================================================== */
/*============================================================
BACKGROUND COLORS
============================================================*/
.db-bk-color-one {
background-color: #D9EDD2;
}
.db-bk-color-two {
background-color: #B9CEA0;
}
.db-bk-color-three {
background-color: #D9EDD2;
}
.db-bk-color-six {
background-color: #F59B24;
}
.db-padding-btm {
padding-bottom: 50px;
}
.db-button-color-square {
color: #fff;
background-color: rgba(0, 0, 0, 0.50);
border: none;
border-radius: 0px;
-webkit-border-radius: 0px;
-moz-border-radius: 0px;
}
.db-button-color-square:hover {
color: #fff;
background-color: rgba(0, 0, 0, 0.50);
border: none;
}
.db-pricing-eleven {
margin-bottom: 30px;
margin-top: 50px;
text-align: center;
box-shadow: 0 0 5px rgba(0, 0, 0, .5);
border-radius: 7px 7px 7px 7px;
color: #101211;
line-height: 30px;
}
.db-pricing-eleven ul {
list-style: none;
margin: 0;
text-align: left;
padding-left: 5px;
}
.db-pricing-eleven ul li {
padding-top: 20px;
padding-bottom: 20px;
cursor: pointer;
}
.db-pricing-eleven ul li i {
margin-right: 5px;
}
.db-pricing-eleven .price {
background-color: rgba(0, 0, 0, 0);
color: #353000;
}
.db-pricing-eleven .price small {
color: #63783F;
display: block;
font-size: 12px;
margin-top: 22px;
}
.db-pricing-eleven .type {
background-color: #63783F;
padding: 50px 20px;
font-weight: 900;
text-transform: uppercase;
}
.text-price-responsive {
font-size: 30px;
padding: 20px 10px 10px 10px;
font-weight: 500;
}
.text-type-responsive {
color: white;
font-size: 20px;
}
.text-responsive {
font-size: 16px;
}
.hide-in-cell {
display: none
}
@media (min-width: 544px) {
.db-pricing-eleven ul {
list-style: none;
margin: 0;
text-align: left;
padding-left: 5px;
}
.text-price-responsive {
font-size: 30px;
padding: 20px 10px 10px 10px;
font-weight: 500;
}
.text-type-responsive {
font-size: 30px;
color: white;
}
.text-responsive {
font-size: 16px;
}
.hide-in-cell {
display: none
}
}
/* Medium devices (tablets, 768px and up) */
@media (min-width: 768px) {
.text-price-responsive {
padding: 40px 20px 20px 20px;
font-size: 40px;
}
.text-type-responsive {
font-size: 30px;
color: white;
}
.text-responsive {
font-size: 20px;
}
}
/* Large devices (desktops, 992px and up) */
@media (min-width: 992px) {
.text-price-responsive {
padding: 40px 20px 20px 20px;
font-size: 60px;
}
.text-type-responsive {
font-size: 30px;
color: white;
}
.text-responsive {
font-size: 20px;
}
}
/* Extra large devices (large desktops, 1200px and up) */
@media (min-width: 1200px) {
.db-pricing-eleven ul {
list-style: none;
margin: 0;
text-align: left;
padding-left: 20px;
}
.text-price-responsive {
padding: 40px 20px 20px 20px;
font-size: 60px;
}
.text-type-responsive {
color: white;
font-size: 30px;
}
.text-responsive {
font-size: 16px;
}
.hide-in-cell {
display: inline
}
}
.db-pricing-eleven .pricing-footer {
padding: 20px;
}
.db-attached > .col-lg-4,
.db-attached > .col-lg-3,
.db-attached > .col-md-4,
.db-attached > .col-md-3,
.db-attached > .col-sm-4,
.db-attached > .col-sm-3 {
padding-left: 0;
padding-right: 0;
}
.db-pricing-eleven.popular {
margin-top: 10px;
}
.db-pricing-eleven.popular .price {
padding-top: 80px;
}

View File

@ -1,109 +1,394 @@
@{
ViewBag.Title = "Planos";
ViewData["Title"] = "Planos";
}
<div class="row db-padding-btm db-attached">
<div class="col-4 col-xs-4 col-sm-4 col-md-4 col-lg-4">
<div class="db-wrapper">
<div class="db-pricing-eleven db-bk-color-one">
<div class="price text-price-responsive">
<sup>R$</sup> 0,00
<small>para sempre!</small>
</div>
<div class="type text-type-responsive">
Básico
</div>
<ul class="text-responsive">
<li><i class="bi bi-plus-square-fill text-success"></i>1 Bio/Links <span class="hide-in-cell">pessoal virtual</span></li>
<li><i class="bi bi-plus-square-fill text-success"></i><span class="hide-in-cell">Exibir imagem </span>Whatsapp *</li>
<li><i class="bi bi-plus-square-fill text-success"></i>Até 3 links </li>
<li><i class="bi bi-square text-success"></i><span class="hide-in-cell">Sem página de</span> Produtos</li>
<li><i class="bi bi-square text-success"></i><span class="hide-in-cell">Sem contador de </span>Visualizações</span></li>
</ul>
<div class="pricing-footer">
@if (User.Identity.IsAuthenticated)
{
<div class="btn db-button-color-square btn-lg">Plano Inicial</div>
@section Styles {
<style type="text/css">
.plans-header {
background-color: var(--suma-beige);
padding: 3rem 0;
border-radius: 0 0 50% 50% / 20px;
margin-bottom: 3rem;
}
else
{
<div class="btn db-button-color-square btn-lg">Comece com este!</div>
}
</div>
</div>
</div>
</div>
<div class="col-4 col-xs-4 col-sm-4 col-md-4 col-lg-4">
<div class="db-wrapper">
<div class="db-pricing-eleven db-bk-color-two popular">
<div class="price text-price-responsive">
<sup>R$</sup>13
<small>por mês</small>
</div>
<div class="type text-type-responsive">
BIO
</div>
<ul>
<li><i class="bi bi-plus-square-fill text-success"></i>1 Bio/Links <span class="hide-in-cell">pessoal virtual</span></li>
<li><i class="bi bi-plus-square-fill text-success"></i><span class="hide-in-cell">Exibir cartão </span>Whatsapp *</li>
<li><i class="bi bi-plus-square-fill text-success"></i>100 links</li>
<li><i class="bi bi-square text-success"></i><span class="hide-in-cell">Sem página de</span> Produtos</li>
@*
<li><i class="bi bi-plus-square-fill text-success"></i>1 Link agendado **</li>
*@
<li><i class="bi bi-square text-success"></i><span class="hide-in-cell">Contador de</span> Visualizações</li>
@*
<li><i class="bi bi-square text-success"></i><span class="hide-in-cell">Registro de contatos</span> </li>
*@
</ul>
<div class="pricing-footer">
@if (User.Identity.IsAuthenticated)
{
<a href="/Pay/?plan=bio" class="btn db-button-color-square btn-lg">Assinar</a>
}
else
{
<a href="/Login" class="btn db-button-color-square btn-lg">Login / Registro</a>
}
</div>
</div>
</div>
</div>
<div class="col-4 col-xs-4 col-sm-4 col-md-4 col-lg-4">
<div class="db-wrapper">
<div class="db-pricing-eleven db-bk-color-three">
<div class="price text-price-responsive">
<sup>R$</sup>27
<small>por mês</small>
</div>
<div class="type text-type-responsive">
CATÁLOGO
</div>
<ul>
<li><i class="bi bi-plus-square-fill text-success"></i>1 Bio/Cartão <span class="hide-in-cell">pessoal virtual</span></li>
<li><i class="bi bi-plus-square-fill text-success"></i><span class="hide-in-cell">Exibir cartão </span>Whatsapp *</li>
<li><i class="bi bi-plus-square-fill text-success"></i>Sem limite de links </li>
<li><i class="bi bi-plus-square-fill text-success"></i>Links de Produtos</li>
<li><i class="bi bi-plus-square-fill text-success"></i>Contador<span class="hide-in-cell"> de visualizações</span> </li>
@*
<li><i class="bi bi-plus-square-fill text-success"></i><span class="hide-in-cell">Registro de contatos</span> </li>
*@
</ul>
<div class="pricing-footer">
@if (User.Identity.IsAuthenticated)
{
<button type="button" class="btn db-button-color-square btn-lg" onclick="waitingDialog.show('Custom message');window.location.href = '/Pay/?plan=catalogo';">Assinar</button>
@*
<a href="/Pay/?plan=catalogo" class="btn db-button-color-square btn-lg">Assinar</a>
*@
.pricing-card {
background-color: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
transition: all 0.3s ease;
height: 100%;
border: 2px solid transparent;
}
else
{
<a href="/Login" class="btn db-button-color-square btn-lg">Login / Registro</a>
.pricing-card:hover {
transform: translateY(-10px);
box-shadow: 0 15px 30px rgba(0,0,0,0.1);
}
.pricing-card.popular {
border-color: var(--suma-red);
position: relative;
}
.popular-badge {
position: absolute;
top: 0;
right: 0;
background-color: var(--suma-red);
color: white;
padding: 5px 15px;
font-size: 0.8rem;
border-radius: 0 12px 0 12px;
}
.pricing-header {
text-align: center;
padding: 1.5rem;
border-bottom: 1px solid var(--suma-gray);
}
.pricing-price {
font-size: 2.5rem;
font-weight: 700;
margin: 1rem 0;
color: var(--suma-red);
}
.pricing-period {
font-size: 0.9rem;
color: var(--suma-dark-gray);
}
.pricing-features {
padding: 1.5rem;
}
.pricing-feature {
display: flex;
align-items: center;
margin-bottom: 1rem;
}
.pricing-feature i {
color: var(--suma-red);
margin-right: 10px;
}
.pricing-feature.disabled {
color: var(--suma-dark-gray);
text-decoration: line-through;
}
.pricing-action {
padding: 1.5rem;
text-align: center;
}
.btn-outline-red {
color: var(--suma-red);
border-color: var(--suma-red);
}
.btn-outline-red:hover {
background-color: var(--suma-red);
color: white;
}
.faq-item {
margin-bottom: 1.5rem;
}
.faq-question {
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--suma-red);
}
</style>
}
<!-- Header da página -->
<div class="plans-header">
<div class="container text-center">
<h1 class="display-4 mb-3">Planos de Assinatura</h1>
<p class="lead mb-4">Escolha o plano ideal para suas necessidades de resumo de vídeos</p>
<div class="d-flex justify-content-center">
<div class="btn-group" role="group">
<button type="button" class="btn btn-light active" id="monthlyBtn">Mensal</button>
<button type="button" class="btn btn-light" id="yearlyBtn">Anual (20% desconto)</button>
</div>
</div>
</div>
</div>
<!-- Cards de planos -->
<div class="container mb-5">
<div class="row">
<!-- Plano Gratuito -->
<div class="col-md-4 mb-4">
<div class="pricing-card">
<div class="pricing-header">
<h3>Gratuito</h3>
<div class="pricing-price">R$ 0</div>
<div class="pricing-period">para sempre</div>
</div>
<div class="pricing-features">
<div class="pricing-feature">
<i class="bi bi-check-circle-fill"></i>
<span>3 resumos por mês</span>
</div>
<div class="pricing-feature">
<i class="bi bi-check-circle-fill"></i>
<span>Resumos básicos</span>
</div>
<div class="pricing-feature">
<i class="bi bi-check-circle-fill"></i>
<span>Vídeos de até 10 minutos</span>
</div>
<div class="pricing-feature disabled">
<i class="bi bi-x-circle"></i>
<span>Download de resumos</span>
</div>
<div class="pricing-feature disabled">
<i class="bi bi-x-circle"></i>
<span>Transcrição completa</span>
</div>
<div class="pricing-feature disabled">
<i class="bi bi-x-circle"></i>
<span>Suporte prioritário</span>
</div>
</div>
<div class="pricing-action">
<a href="#" class="btn btn-outline-red btn-lg btn-block">Começar grátis</a>
</div>
</div>
</div>
<!-- Plano Premium -->
<div class="col-md-4 mb-4">
<div class="pricing-card popular">
<div class="popular-badge">Mais popular</div>
<div class="pricing-header">
<h3>Premium</h3>
<div class="pricing-price" id="premiumPrice">R$ 29,90</div>
<div class="pricing-period" id="premiumPeriod">por mês</div>
</div>
<div class="pricing-features">
<div class="pricing-feature">
<i class="bi bi-check-circle-fill"></i>
<span>Resumos ilimitados</span>
</div>
<div class="pricing-feature">
<i class="bi bi-check-circle-fill"></i>
<span>Resumos detalhados</span>
</div>
<div class="pricing-feature">
<i class="bi bi-check-circle-fill"></i>
<span>Vídeos de até 3 horas</span>
</div>
<div class="pricing-feature">
<i class="bi bi-check-circle-fill"></i>
<span>Download de resumos (PDF)</span>
</div>
<div class="pricing-feature">
<i class="bi bi-check-circle-fill"></i>
<span>Transcrição completa</span>
</div>
<div class="pricing-feature disabled">
<i class="bi bi-x-circle"></i>
<span>Suporte prioritário</span>
</div>
</div>
<div class="pricing-action">
<a href="#" class="btn btn-primary btn-lg btn-block">Assinar agora</a>
</div>
</div>
</div>
<!-- Plano Pro -->
<div class="col-md-4 mb-4">
<div class="pricing-card">
<div class="pricing-header">
<h3>Profissional</h3>
<div class="pricing-price" id="proPrice">R$ 59,90</div>
<div class="pricing-period" id="proPeriod">por mês</div>
</div>
<div class="pricing-features">
<div class="pricing-feature">
<i class="bi bi-check-circle-fill"></i>
<span>Tudo do plano Premium</span>
</div>
<div class="pricing-feature">
<i class="bi bi-check-circle-fill"></i>
<span>Resumos para vídeos de qualquer duração</span>
</div>
<div class="pricing-feature">
<i class="bi bi-check-circle-fill"></i>
<span>Análise avançada de conteúdo</span>
</div>
<div class="pricing-feature">
<i class="bi bi-check-circle-fill"></i>
<span>Extração de pontos-chave personalizados</span>
</div>
<div class="pricing-feature">
<i class="bi bi-check-circle-fill"></i>
<span>API para integração</span>
</div>
<div class="pricing-feature">
<i class="bi bi-check-circle-fill"></i>
<span>Suporte prioritário 24/7</span>
</div>
</div>
<div class="pricing-action">
<a href="#" class="btn btn-outline-red btn-lg btn-block">Experimente 7 dias grátis</a>
</div>
</div>
</div>
</div>
<!-- Tabela comparativa -->
<div class="mt-5 mb-5">
<h3 class="text-center mb-4">Comparação detalhada dos planos</h3>
<div class="table-responsive">
<table class="table table-hover">
<thead class="thead-light">
<tr>
<th>Recurso</th>
<th class="text-center">Gratuito</th>
<th class="text-center">Premium</th>
<th class="text-center">Profissional</th>
</tr>
</thead>
<tbody>
<tr>
<td>Resumos mensais</td>
<td class="text-center">3</td>
<td class="text-center">Ilimitados</td>
<td class="text-center">Ilimitados</td>
</tr>
<tr>
<td>Duração máxima dos vídeos</td>
<td class="text-center">10 minutos</td>
<td class="text-center">3 horas</td>
<td class="text-center">Sem limite</td>
</tr>
<tr>
<td>Transcrição completa</td>
<td class="text-center"><i class="bi bi-x text-danger"></i></td>
<td class="text-center"><i class="bi bi-check text-success"></i></td>
<td class="text-center"><i class="bi bi-check text-success"></i></td>
</tr>
<tr>
<td>Download de resumos</td>
<td class="text-center"><i class="bi bi-x text-danger"></i></td>
<td class="text-center"><i class="bi bi-check text-success"></i></td>
<td class="text-center"><i class="bi bi-check text-success"></i></td>
</tr>
<tr>
<td>Palavras-chave e tópicos</td>
<td class="text-center">Básico</td>
<td class="text-center">Avançado</td>
<td class="text-center">Personalizado</td>
</tr>
<tr>
<td>Acesso a API</td>
<td class="text-center"><i class="bi bi-x text-danger"></i></td>
<td class="text-center"><i class="bi bi-x text-danger"></i></td>
<td class="text-center"><i class="bi bi-check text-success"></i></td>
</tr>
<tr>
<td>Suporte</td>
<td class="text-center">Email</td>
<td class="text-center">Email e chat</td>
<td class="text-center">Prioritário 24/7</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- FAQs -->
<div class="container mb-5">
<h3 class="text-center mb-4">Perguntas Frequentes</h3>
<div class="row">
<div class="col-md-6">
<div class="faq-item">
<h5 class="faq-question">Como funciona o SumaTube?</h5>
<p>O SumaTube usa tecnologia avançada de IA para extrair e analisar as legendas dos vídeos do YouTube, criando resumos inteligentes que capturam os pontos principais do conteúdo.</p>
</div>
<div class="faq-item">
<h5 class="faq-question">Posso cancelar minha assinatura a qualquer momento?</h5>
<p>Sim, você pode cancelar sua assinatura a qualquer momento. Não há contratos de longo prazo ou taxas de cancelamento.</p>
</div>
<div class="faq-item">
<h5 class="faq-question">Como são processados os pagamentos?</h5>
<p>Processamos pagamentos via cartão de crédito, débito, PayPal e Pix. Todas as transações são seguras e criptografadas.</p>
</div>
</div>
<div class="col-md-6">
<div class="faq-item">
<h5 class="faq-question">E se o vídeo não tiver legendas?</h5>
<p>O SumaTube pode gerar legendas automaticamente para vídeos que não as possuem, embora a precisão possa variar dependendo da qualidade do áudio.</p>
</div>
<div class="faq-item">
<h5 class="faq-question">Quais idiomas são suportados?</h5>
<p>Atualmente suportamos resumos em português, inglês e espanhol. Estamos constantemente adicionando suporte para novos idiomas.</p>
</div>
<div class="faq-item">
<h5 class="faq-question">Como posso entrar em contato com o suporte?</h5>
<p>Você pode entrar em contato conosco através do email suporte@sumatube.com ou pelo chat disponível no site para usuários Premium e Profissional.</p>
</div>
</div>
</div>
<!-- Call to Action -->
<div class="text-center mt-5">
<h3 class="mb-3">Pronto para começar?</h3>
<p class="mb-4">Escolha o plano ideal para suas necessidades e comece a usar o SumaTube hoje mesmo.</p>
<a href="#" class="btn btn-primary btn-lg">Criar conta gratuita</a>
</div>
</div>
@section Scripts {
<script type="text/javascript">
$(document).ready(function() {
// Toggle entre planos mensais e anuais
$('#monthlyBtn').click(function() {
$(this).addClass('active');
$('#yearlyBtn').removeClass('active');
// Atualizar preços para mensais
$('#premiumPrice').text('R$ 29,90');
$('#premiumPeriod').text('por mês');
$('#proPrice').text('R$ 59,90');
$('#proPeriod').text('por mês');
});
$('#yearlyBtn').click(function() {
$(this).addClass('active');
$('#monthlyBtn').removeClass('active');
// Atualizar preços para anuais com desconto
$('#premiumPrice').text('R$ 287,04');
$('#premiumPeriod').text('por ano (R$ 23,92/mês)');
$('#proPrice').text('R$ 575,04');
$('#proPeriod').text('por ano (R$ 47,92/mês)');
});
// Animação aos cards
$('.pricing-card').hover(
function() {
$(this).addClass('shadow-lg');
},
function() {
$(this).removeClass('shadow-lg');
}
);
});
</script>
}

View File

@ -1,7 +1,7 @@
@using System.Security.Claims
<!DOCTYPE html>
<html lang="en">
<html lang="pt-br">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
@ -9,110 +9,181 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css" integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N" crossorigin="anonymous">
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/css/custom.css" asp-append-version="true" />
<link rel="stylesheet" href="~/SumaTube.styles.css" asp-append-version="true" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
@await RenderSectionAsync("Styles", required: false)
</head>
<body class="hide-body">
<body class="d-flex flex-column min-vh-100 hide-body">
<partial name="_Busy" />
<div id="wrapper">
<header>
<nav id="nav-bar" class="navbar navbar-expand-lg navbar-dark">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">SumaTube</a>
<div class="container">
<a class="navbar-brand d-flex align-items-center" asp-area="" asp-controller="Home" asp-action="Index">
<i class="bi bi-play-btn-fill mr-2"></i> SumaTube
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<!-- Campo de busca central -->
<div class="d-none d-md-block mx-auto">
<form class="form-inline my-2 my-lg-0">
<div class="input-group">
<input class="form-control search-input" type="search" placeholder="Buscar vídeos..." aria-label="Buscar">
<div class="input-group-append">
<button class="btn btn-light" type="submit">
<i class="bi bi-search"></i>
</button>
</div>
</div>
</form>
</div>
<div class="navbar-collapse collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link text-white" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
<a class="nav-link text-white" asp-area="" asp-controller="Home" asp-action="Index">
<i class="bi bi-house-door"></i> Home
</a>
</li>
@*
Exibir apenas são não estiver logado.
*@
<li class="nav-item">
<a class="nav-link text-white" asp-area="" asp-controller="Plans" asp-action="Index">
<i class="bi bi-star"></i> Planos
</a>
</li>
<li class="nav-item">
<a class="nav-link text-white" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
<a class="nav-link text-white" asp-area="" asp-controller="Video" asp-action="MySummary">
<i class="bi bi-file-text"></i> Meus resumos
</a>
</li>
<li class="nav-item">
<a class="nav-link text-white" asp-area="" asp-controller="Plans" asp-action="Index">Planos</a>
<a class="nav-link text-white" asp-area="" asp-controller="Video" asp-action="Extract">
<i class="bi bi-magic"></i> Extract
</a>
</li>
<partial name="_Language" />
</ul>
@if (User != null && User.Identity != null && !User.Identity.IsAuthenticated)
{
<ul class="navbar-nav ml-auto">
<partial name="_Language" />
<li class="nav-item" style="margin-right: 10px">
<a class="nav-link text-white" asp-area="" asp-controller="Login" asp-action="Index"><i class="bi bi-person"></i> Login</a>
<li class="nav-item">
<a class="nav-link text-white" asp-area="" asp-controller="Login" asp-action="Index">
<i class="bi bi-box-arrow-in-right"></i> Login
</a>
</li>
</ul>
}
else
{
<ul class="navbar-nav ml-auto">
<partial name="_Language"/>
<li class="nav-item dropdown" style="margin-right: 10px">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="bi bi-person"></i> @(User.FindFirst("FullName")!=null ? User.FindFirst("FullName").Value : "N/A")
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" asp-area="" asp-controller="Login" asp-action="Logout">Sair</a>
@*
<a class="dropdown-item" href="#">Action</a>
<a class="dropdown-item" href="#">Another action</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#">Something else here</a>
Criar uma tela que permita alterar o plano.
*@
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle text-white" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="bi bi-person-circle"></i> @(User.FindFirst("FullName") != null ? User.FindFirst("FullName").Value : "N/A")
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
<a class="dropdown-item" asp-controller="Account" asp-action="Profile">
<i class="bi bi-person"></i> Meu Perfil
</a>
<a class="dropdown-item" asp-controller="Videos" asp-action="History">
<i class="bi bi-clock-history"></i> Histórico
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" asp-area="" asp-controller="Login" asp-action="Logout">
<i class="bi bi-box-arrow-right"></i> Sair
</a>
</div>
</li>
@*
<li class="nav-item">
<a class="nav-link text-white" asp-area="" asp-controller="Login" asp-action="Logout">@User.FindFirst("FullName").Value
</a>
</li>
*@
</ul>
}
</div>
</nav>
<div class="container">
@RenderBody()
@*
<main role="main" class="pb-3">
</main>
*@
</div>
</nav>
<footer class="border-top footer text-muted">
<!-- Campo de busca para dispositivos móveis -->
<div class="container d-md-none mt-3 mb-3">
<form class="form-inline">
<div class="input-group w-100">
<input class="form-control" type="search" placeholder="Buscar vídeos..." aria-label="Buscar">
<div class="input-group-append">
<button class="btn btn-light" type="submit">
<i class="bi bi-search"></i>
</button>
</div>
</div>
</form>
</div>
</header>
<!-- Conteúdo principal -->
<main class="flex-grow-1">
<div class="container py-4">
@RenderBody()
</div>
</main>
<!-- Footer -->
<footer class="footer mt-auto py-3">
<div class="container">
&copy; 2024 - SumaTube - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
<div class="row align-items-center">
<div class="col-md-4 mb-3 mb-md-0">
<div class="d-flex align-items-center">
<i class="bi bi-play-btn-fill mr-2" style="color: var(--suma-red); font-size: 1.2rem;"></i>
<span class="font-weight-bold">SumaTube</span>
<span class="mx-2">|</span>
<small>Resumos inteligentes de vídeos</small>
</div>
</div>
<div class="col-md-4 mb-3 mb-md-0 text-center">
<div class="d-flex justify-content-center">
<a href="#" class="text-muted mx-2"><i class="bi bi-facebook"></i></a>
<a href="#" class="text-muted mx-2"><i class="bi bi-twitter"></i></a>
<a href="#" class="text-muted mx-2"><i class="bi bi-instagram"></i></a>
<a href="#" class="text-muted mx-2"><i class="bi bi-youtube"></i></a>
</div>
</div>
<div class="col-md-4 text-md-right">
<div class="text-center text-md-right">
<small class="text-muted">&copy; 2024 - Todos os direitos reservados</small>
</div>
</div>
</div>
</div>
</footer>
</div>
<!-- Scripts -->
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-Fy6S3B9q64WdZWQUiU+q4/2Lc9npb8tCaSX9FK7E8HnRr0Jz8D6OP9dO5Vg3Q9ct" crossorigin="anonymous"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
<script src="~/js/wait.js" asp-append-version="true"></script>
<script type="text/javascript">
$(function () {
$(document).ready(function () {
$('.loading').hide();
//$('body').fadeIn(1000);
$('body').slideDown('slow');
$('body').fadeIn(800);
});
$('a[href="#search"]').on('click', function (event) {
event.preventDefault();
$('#search').addClass('open');
$('#search > form > input[type="search"]').focus();
inactivateActiveOption();
searchActive();
});
// Animação para links do menu
$('.navbar-nav .nav-link').hover(
function() { $(this).addClass('pulse'); },
function() { $(this).removeClass('pulse'); }
);
$('#search, #search button.close').on('click keyup', function (event) {
if (event.target == this || event.target.className == 'close' || event.keyCode == 27) {
$(this).removeClass('open');
// Ativar o item de menu atual
$(document).ready(function () {
setActiveByLocation();
searchInactive();
}
});
// Controle de carregamento de página
$(window).on('beforeunload', function () {
displayBusyIndicator();
});
@ -121,20 +192,10 @@
displayBusyIndicator();
});
$(document).ready(function () {
$('#wrapper').fadeIn('slow');
$('a.nav-link').click(function () {
$('#wrapper').fadeOut('slow');
});
setActiveByLocation();
});
function setActiveByLocation() {
$('ul.navbar-nav').find('a[href="' + location.pathname + '"]')
.closest('li')
.addClass("active")
.addClass("active");
$('ul.navbar-nav').find('a[href="' + location.pathname + '"]')
.closest('a')
@ -142,37 +203,9 @@
.addClass('text-black');
}
function searchActive() {
$('#searchLi').addClass("active");
$('#searchLi > a')
.removeClass('text-white')
.addClass('text-black');
}
function searchInactive() {
$('#searchLi').removeClass("active");
$('#searchLi > a')
.removeClass('text-black')
.addClass('text-white');
}
function inactivateActiveOption() {
$('ul.navbar-nav li.active')
.closest('li')
.removeClass("active")
$('ul.navbar-nav').find('a[href="' + location.pathname + '"]')
.closest('a')
.removeClass('text-black')
.addClass('text-white');
}
function displayBusyIndicator() {
//$('body').fadeOut(1000);
$('body').slideUp('slow');
$('body').fadeOut(300);
}
//$(".hide-body").fadeOut(2000);
});
</script>
@await RenderSectionAsync("Scripts", required: false)

View File

@ -44,14 +44,6 @@ button.accept-policy {
line-height: inherit;
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
white-space: nowrap;
line-height: 60px;
}
#search {
position: fixed;
top: 0px;

View File

@ -0,0 +1,273 @@
@{
ViewData["Title"] = "Extrair Resumo";
}
@section Styles {
<style type="text/css">
.extract-container {
max-width: 800px;
margin: 2rem auto;
}
.extract-card {
background-color: white;
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
padding: 2rem;
}
.form-control-lg {
font-size: 1.1rem;
padding: 1rem 1.5rem;
border-radius: 30px 0 0 30px;
}
.btn-extract {
border-radius: 0 30px 30px 0;
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.feature-icon {
background-color: var(--suma-beige);
border-radius: 50%;
width: 70px;
height: 70px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1rem auto;
}
.feature-icon i {
font-size: 2rem;
color: var(--suma-red);
}
.step-number {
background-color: var(--suma-red);
color: white;
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
margin-right: 1rem;
}
.progress-container {
display: none;
margin-top: 2rem;
}
.progress {
height: 10px;
border-radius: 5px;
}
</style>
}
<div class="container extract-container">
<div class="text-center mb-5">
<h1 class="display-4 mb-3">Resumir Vídeo do YouTube</h1>
<p class="lead">Cole o link de qualquer vídeo do YouTube para obter um resumo inteligente</p>
</div>
<div class="extract-card mb-5">
<form id="extractForm">
<div class="form-group">
<label for="youtubeUrl"><strong>URL do vídeo</strong></label>
<div class="input-group input-group-lg">
<input type="url" class="form-control form-control-lg" id="youtubeUrl" placeholder="https://www.youtube.com/watch?v=..." required>
<div class="input-group-append">
<button type="submit" class="btn btn-primary btn-lg btn-extract" id="extractButton">
<i class="bi bi-magic mr-2"></i> Resumir
</button>
</div>
</div>
<small class="form-text text-muted">Ex: https://www.youtube.com/watch?v=abcde12345</small>
</div>
<div class="progress-container" id="progressContainer">
<p class="text-center mb-2" id="processingStatus">Processando vídeo...</p>
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated bg-danger" id="progressBar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%"></div>
</div>
</div>
</form>
</div>
<!-- Como funciona -->
<div class="mb-5">
<h3 class="text-center mb-4">Como funciona</h3>
<div class="row">
<!-- Passo 1 -->
<div class="col-md-4 mb-4">
<div class="feature-icon">
<i class="bi bi-link-45deg"></i>
</div>
<h5 class="text-center mb-3">Passo 1</h5>
<div class="d-flex align-items-start">
<div class="step-number">1</div>
<div>
Cole o link do vídeo do YouTube que deseja resumir no campo acima.
</div>
</div>
</div>
<!-- Passo 2 -->
<div class="col-md-4 mb-4">
<div class="feature-icon">
<i class="bi bi-cpu"></i>
</div>
<h5 class="text-center mb-3">Passo 2</h5>
<div class="d-flex align-items-start">
<div class="step-number">2</div>
<div>
Nossa IA extrai as legendas e processa o conteúdo do vídeo automaticamente.
</div>
</div>
</div>
<!-- Passo 3 -->
<div class="col-md-4 mb-4">
<div class="feature-icon">
<i class="bi bi-file-earmark-text"></i>
</div>
<h5 class="text-center mb-3">Passo 3</h5>
<div class="d-flex align-items-start">
<div class="step-number">3</div>
<div>
Receba um resumo completo com os principais pontos, timestamps e transcrição.
</div>
</div>
</div>
</div>
</div>
<!-- Recursos -->
<div class="row">
<div class="col-12 text-center mb-4">
<h3>Recursos incríveis do SumaTube</h3>
</div>
<!-- Recurso 1 -->
<div class="col-md-6 mb-4">
<div class="summary-section d-flex">
<div class="mr-4">
<i class="bi bi-clock" style="font-size: 2rem; color: var(--suma-red);"></i>
</div>
<div>
<h5>Economize tempo</h5>
<p>Extraia o conteúdo mais importante de vídeos longos em questão de minutos.</p>
</div>
</div>
</div>
<!-- Recurso 2 -->
<div class="col-md-6 mb-4">
<div class="summary-section d-flex">
<div class="mr-4">
<i class="bi bi-translate" style="font-size: 2rem; color: var(--suma-red);"></i>
</div>
<div>
<h5>Suporte a múltiplos idiomas</h5>
<p>Resumos disponíveis em português, inglês, espanhol e outros idiomas.</p>
</div>
</div>
</div>
<!-- Recurso 3 -->
<div class="col-md-6 mb-4">
<div class="summary-section d-flex">
<div class="mr-4">
<i class="bi bi-lightning-charge" style="font-size: 2rem; color: var(--suma-red);"></i>
</div>
<div>
<h5>Processamento rápido</h5>
<p>Nossa tecnologia avançada processa os vídeos em segundos.</p>
</div>
</div>
</div>
<!-- Recurso 4 -->
<div class="col-md-6 mb-4">
<div class="summary-section d-flex">
<div class="mr-4">
<i class="bi bi-download" style="font-size: 2rem; color: var(--suma-red);"></i>
</div>
<div>
<h5>Download de resumos</h5>
<p>Salve os resumos em PDF para referência futura e compartilhamento.</p>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
<script type="text/javascript">
$(document).ready(function() {
$('#extractForm').submit(function(e) {
e.preventDefault();
// Validar URL
var youtubeUrl = $('#youtubeUrl').val();
if (!isValidYouTubeUrl(youtubeUrl)) {
alert('Por favor, insira uma URL válida do YouTube.');
return;
}
// Mostrar progresso
$('#extractButton').prop('disabled', true);
$('#progressContainer').show();
simulateProgress();
// Simulação de processamento - em um projeto real, aqui você faria a chamada AJAX para sua WebAPI
setTimeout(function() {
// Redirecionar para a página de resultado (em um caso real, você redirecionaria após receber a resposta da API)
window.location.href = '/Video/Summary?id=' + extractVideoId(youtubeUrl);
}, 5000);
});
function isValidYouTubeUrl(url) {
var regex = /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+/;
return regex.test(url);
}
function extractVideoId(url) {
var regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
var match = url.match(regExp);
return (match && match[7].length == 11) ? match[7] : false;
}
function simulateProgress() {
var progress = 0;
var interval = setInterval(function() {
progress += 5;
$('#progressBar').css('width', progress + '%');
$('#progressBar').attr('aria-valuenow', progress);
// Atualizar mensagem de status
if (progress <= 20) {
$('#processingStatus').text('Extraindo informações do vídeo...');
} else if (progress <= 40) {
$('#processingStatus').text('Processando legendas...');
} else if (progress <= 60) {
$('#processingStatus').text('Identificando pontos principais...');
} else if (progress <= 80) {
$('#processingStatus').text('Gerando resumo...');
} else {
$('#processingStatus').text('Finalizando...');
}
if (progress >= 100) {
clearInterval(interval);
}
}, 200);
}
});
</script>
}

View File

@ -0,0 +1,83 @@
@using SumaTube.Domain.Entities.Videos
@model List<VideoSummary>
@{
ViewData["Title"] = "Meus Resumos";
}
<div class="container my-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Meus Resumos</h1>
<a href="@Url.Action("Extract", "Video")" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Novo Resumo
</a>
</div>
@if (Model == null || !Model.Any())
{
<div class="text-center py-5">
<i class="bi bi-file-earmark-text" style="font-size: 3rem; color: var(--suma-red);"></i>
<h3 class="mt-3">Você ainda não possui resumos</h3>
<p class="text-muted">Clique em "Novo Resumo" para extrair o conteúdo de um vídeo do YouTube</p>
<a href="@Url.Action("Extract", "Video")" class="btn btn-primary mt-3">
<i class="bi bi-plus-circle"></i> Criar Meu Primeiro Resumo
</a>
</div>
}
else
{
<div class="row">
@foreach (var summary in Model)
{
<div class="col-md-6 col-lg-4 mb-4">
<div class="card h-100 shadow-sm">
<div class="position-relative">
<img src="@summary.ThumbnailUrl" class="card-img-top" alt="@summary.Title">
@if (summary.Status == "PROCESSANDO")
{
<div class="position-absolute top-0 end-0 p-2 bg-warning text-dark rounded-bottom-left">
<i class="bi bi-hourglass-split"></i> Processando
</div>
}
else if (summary.Status == "REALIZADO")
{
<div class="position-absolute top-0 end-0 p-2 bg-success text-white rounded-bottom-left">
<i class="bi bi-check-circle"></i> Concluído
</div>
}
else if (summary.Status == "ERRO")
{
<div class="position-absolute top-0 end-0 p-2 bg-danger text-white rounded-bottom-left">
<i class="bi bi-exclamation-triangle"></i> Erro
</div>
}
</div>
<div class="card-body">
<h5 class="card-title text-truncate" title="@summary.Title">@summary.Title</h5>
<p class="card-text text-muted small">
<i class="bi bi-calendar"></i> @summary.RequestDate.ToString("dd/MM/yyyy HH:mm")
<br>
<i class="bi bi-translate"></i> @summary.Language
</p>
</div>
<div class="card-footer bg-white border-top-0">
<a href="@Url.Action("Summary", "Video", new { id = summary.Id })" class="btn btn-sm btn-outline-primary w-100">
@if (summary.Status == "PROCESSANDO")
{
<span>Ver Progresso</span>
}
else if (summary.Status == "REALIZADO")
{
<span>Ver Resumo</span>
}
else
{
<span>Ver Detalhes</span>
}
</a>
</div>
</div>
</div>
}
</div>
}
</div>

View File

@ -0,0 +1,260 @@
@using SumaTube.Models;
@model VideoSummaryViewModel
@{
ViewData["Title"] = "Resumo do Vídeo";
}
@section Styles {
<style type="text/css">
.timestamp-link {
color: var(--suma-red);
cursor: pointer;
text-decoration: none;
}
.timestamp-link:hover {
text-decoration: underline;
}
.summary-section {
background-color: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.tag-pill {
background-color: var(--suma-beige);
border-radius: 20px;
padding: 5px 15px;
margin-right: 8px;
margin-bottom: 8px;
display: inline-block;
font-size: 0.9rem;
}
.summary-tab-content {
min-height: 300px;
}
.video-frame {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.transcription-text {
max-height: 300px;
overflow-y: auto;
background-color: var(--suma-light-gray);
padding: 15px;
border-radius: 8px;
line-height: 1.7;
}
</style>
}
<div class="container mt-4">
<div class="row">
<!-- Coluna do vídeo -->
<div class="col-lg-8">
<div class="video-frame mb-4">
<div class="embed-responsive embed-responsive-16by9">
<iframe class="embed-responsive-item" src="https://www.youtube.com/embed/@Model.VideoId" allowfullscreen></iframe>
</div>
</div>
<div class="summary-section">
<h1>@Model.VideoTitle</h1>
<div class="d-flex align-items-center mb-3">
<img src="@Model.ChannelThumbnail" alt="@Model.ChannelName" class="rounded-circle mr-2" style="width: 40px; height: 40px;">
<div>
<h6 class="mb-0">@Model.ChannelName</h6>
<small class="text-muted">@Model.PublishedDate • @Model.ViewCount visualizações</small>
</div>
</div>
<div class="d-flex justify-content-between align-items-center mt-3 mb-3">
<div>
<span class="mr-3">
<i class="bi bi-hand-thumbs-up"></i> @Model.LikeCount
</span>
<span>
<i class="bi bi-chat-left-text"></i> @Model.CommentCount
</span>
</div>
<div>
<button class="btn btn-sm btn-outline-dark mr-2">
<i class="bi bi-share"></i> Compartilhar
</button>
<button class="btn btn-sm btn-outline-dark">
<i class="bi bi-bookmark"></i> Salvar
</button>
</div>
</div>
</div>
<!-- Tabs de navegação -->
<ul class="nav nav-tabs" id="summaryTabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="summary-tab" data-toggle="tab" href="#summary" role="tab" aria-controls="summary" aria-selected="true">
<i class="bi bi-journal-text"></i> Resumo
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="transcription-tab" data-toggle="tab" href="#transcription" role="tab" aria-controls="transcription" aria-selected="false">
<i class="bi bi-body-text"></i> Transcrição
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="keywords-tab" data-toggle="tab" href="#keywords" role="tab" aria-controls="keywords" aria-selected="false">
<i class="bi bi-tags"></i> Palavras-chave
</a>
</li>
</ul>
<!-- Conteúdo das tabs -->
<div class="tab-content summary-tab-content p-3 bg-white border border-top-0 rounded-bottom mb-4" id="summaryTabsContent">
<!-- Tab Resumo -->
<div class="tab-pane fade show active" id="summary" role="tabpanel" aria-labelledby="summary-tab">
<h4 class="mb-3">Principais pontos</h4>
@foreach (var point in Model.KeyPoints)
{
<div class="mb-3">
<p>
@* <a class="timestamp-link" data-time="@point.TimestampSeconds">@point.TimestampFormatted</a>
<strong>@point.Title</strong>
</p>
<p>@point.Description</p>
*@
<a class="timestamp-link" data-time="30">30s</a>
<strong>Titulo</strong>
</p>
<p>Description</p>
</div>
}
<h4 class="mt-4 mb-3">Resumo geral</h4>
<p>@Model.SummaryText</p>
</div>
<!-- Tab Transcrição -->
<div class="tab-pane fade" id="transcription" role="tabpanel" aria-labelledby="transcription-tab">
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center">
<h4 class="mb-3">Transcrição completa</h4>
<button class="btn btn-sm btn-outline-secondary">
<i class="bi bi-download"></i> Baixar
</button>
</div>
<div class="transcription-text">
@foreach (var caption in Model.Captions)
{
<p>
@*
<a class="timestamp-link" data-time="@caption.TimestampSeconds">
@caption.TimestampFormatted
</a>
@caption.Text
*@
<a class="timestamp-link" data-time="30s">
30s
</a>
caption.Text
</p>
}
</div>
</div>
</div>
<!-- Tab Palavras-chave -->
<div class="tab-pane fade" id="keywords" role="tabpanel" aria-labelledby="keywords-tab">
<h4 class="mb-3">Tópicos principais</h4>
<div class="mb-4">
@foreach (var keyword in Model.Keywords)
{
<span class="tag-pill">@keyword</span>
}
</div>
<h4 class="mb-3">Temas relacionados</h4>
<div>
@foreach (var topic in Model.RelatedTopics)
{
<span class="tag-pill">@topic</span>
}
</div>
</div>
</div>
</div>
<!-- Coluna lateral -->
<div class="col-lg-4">
<!-- Vídeos recomendados -->
<div class="summary-section">
<h5 class="mb-3">Vídeos relacionados</h5>
@foreach (var video in Model.RelatedVideos)
{
<div class="media mb-3">
@*
<img src="@video.ThumbnailUrl" class="mr-3" alt="@video.Title" style="width: 120px; border-radius: 4px;">
<div class="media-body">
<h6 class="mt-0">@video.Title</h6>
<small class="text-muted">@video.ChannelName</small><br>
<small class="text-muted">@video.ViewCount visualizações</small>
</div>
*@
<img src="#ThumbnailUrl" class="mr-3" alt="#Title" style="width: 120px; border-radius: 4px;">
<div class="media-body">
<h6 class="mt-0">Title</h6>
<small class="text-muted">video.ChannelName</small><br>
<small class="text-muted">x visualizações</small>
</div>
</div>
}
</div>
<!-- Call to action para planos premium -->
<div class="summary-section text-center" style="background-color: var(--suma-beige);">
<i class="bi bi-star-fill mb-3" style="font-size: 2rem; color: var(--suma-red);"></i>
<h5>Obtenha mais recursos com o plano Premium</h5>
<p class="small mb-3">Acesso ilimitado a resumos, download de textos e muito mais.</p>
<a href="#" class="btn btn-primary btn-block">Ver planos</a>
</div>
</div>
</div>
</div>
@section Scripts {
<script type="text/javascript">
$(document).ready(function() {
// Função para clicar nos timestamps e pular para o momento do vídeo
$('.timestamp-link').click(function() {
var time = $(this).data('time');
var videoFrame = $('iframe')[0];
var videoUrl = videoFrame.src;
// Garantir que a URL tem os parâmetros necessários
if (videoUrl.indexOf('?') === -1) {
videoUrl += '?';
} else {
videoUrl += '&';
}
// Adicionar comando para pular para o tempo específico
videoUrl += 'start=' + time;
// Atualizar o iframe
videoFrame.src = videoUrl;
});
});
</script>
}

View File

@ -0,0 +1,255 @@
/* Cores principais do SumaTube */
:root {
--suma-red: #cc0000;
--suma-light-red: #e60000;
--suma-dark-red: #990000;
--suma-beige: #f5f5dc;
--suma-light-gray: #f8f8f8;
--suma-gray: #e0e0e0;
--suma-dark-gray: #606060;
--suma-black: #212121;
}
/* Estilos Gerais */
html, body {
height: 100%;
font-family: 'Roboto', 'Segoe UI', sans-serif;
margin: 0;
padding: 0;
}
body {
background-color: var(--suma-light-gray);
color: var(--suma-black);
display: flex;
flex-direction: column;
min-height: 100vh;
}
.hide-body {
display: none;
}
/* Barra de Navegação */
#nav-bar {
background-color: var(--suma-red) !important;
padding: 0.5rem 1rem;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.navbar-brand {
font-size: 1.5rem;
font-weight: 700;
color: white !important;
}
.navbar-nav .nav-item {
margin: 0 5px;
}
.navbar-nav .nav-item.active {
background-color: rgba(255, 255, 255, 0.2);
border-radius: 4px;
}
.navbar-nav .nav-link {
padding: 8px 16px !important;
border-radius: 3px;
transition: all 0.2s ease;
}
.navbar-nav .nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.navbar-nav .nav-link.text-black {
color: white !important;
font-weight: 500;
}
/* Conteúdo principal */
main {
flex: 1 0 auto;
display: flex;
flex-direction: column;
}
.container {
padding: 20px 15px;
max-width: 1280px;
}
/* Footer */
.footer {
flex-shrink: 0;
background-color: var(--suma-beige);
color: var(--suma-dark-gray);
padding: 20px 0;
width: 100%;
margin-top: auto;
}
/* Login container */
.login-container {
max-width: 500px;
margin: 60px auto;
background-color: white;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
padding: 30px;
text-align: center;
}
/* Seção de hero banner */
.hero-banner {
background: linear-gradient(135deg, var(--suma-red), var(--suma-dark-red));
color: white;
padding: 2.5rem 0;
margin-bottom: 2rem;
border-radius: 8px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
position: relative;
overflow: hidden;
}
.hero-banner::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 60%;
height: 200%;
background: rgba(255, 255, 255, 0.1);
transform: rotate(-15deg);
pointer-events: none;
}
.hero-banner h1 {
font-weight: 700;
font-size: 2.2rem;
margin-bottom: 1rem;
text-shadow: 0 2px 3px rgba(0, 0, 0, 0.2);
}
.hero-banner .lead {
font-size: 1.15rem;
font-weight: 300;
margin-bottom: 1.5rem;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
/* Botões */
.btn-primary, .btn-red {
background-color: var(--suma-red) !important;
border-color: var(--suma-dark-red) !important;
padding: 8px 16px;
border-radius: 4px;
transition: all 0.2s ease;
}
.btn-primary:hover, .btn-red:hover {
background-color: var(--suma-light-red) !important;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.btn-hero {
background-color: white;
color: var(--suma-red);
border: none;
padding: 0.8rem 1.5rem;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.btn-hero:hover {
transform: translateY(-2px);
background-color: #f8f8f8;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.25);
color: var(--suma-dark-red);
}
/* Responsividade */
@media (max-width: 768px) {
.hero-banner {
padding: 2rem 0;
text-align: center;
}
.hero-banner h1 {
font-size: 1.8rem;
}
.btn-hero {
display: block;
width: 100%;
}
}
/* Animações */
.pulse {
animation: pulse 0.5s ease-in-out;
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
/* Efeito de loading */
.loading {
position: fixed;
z-index: 9999;
height: 2em;
width: 2em;
overflow: show;
margin: auto;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.loading:before {
content: '';
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(245, 245, 220, 0.7);
}
.loading:not(:required):after {
content: '';
display: block;
font-size: 10px;
width: 1em;
height: 1em;
margin-top: -0.5em;
animation: spinner 1500ms infinite linear;
border-radius: 0.5em;
box-shadow: rgba(204, 0, 0, 0.75) 1.5em 0 0 0, rgba(204, 0, 0, 0.75) 1.1em 1.1em 0 0, rgba(204, 0, 0, 0.75) 0 1.5em 0 0, rgba(204, 0, 0, 0.75) -1.1em 1.1em 0 0, rgba(204, 0, 0, 0.75) -1.5em 0 0 0, rgba(204, 0, 0, 0.75) -1.1em -1.1em 0 0, rgba(204, 0, 0, 0.75) 0 -1.5em 0 0, rgba(204, 0, 0, 0.75) 1.1em -1.1em 0 0;
}
/* Animação do spinner */
@keyframes spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB