feat: modularização

This commit is contained in:
Ricardo Carneiro 2025-06-08 18:00:23 -03:00
parent 6085f1b117
commit 6b79a44e39
14 changed files with 1526 additions and 209 deletions

View File

@ -20,5 +20,10 @@ namespace OnlyOneAccessTemplate.Models
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public string? JavaScriptUrl { get; set; } // URL do arquivo JS
public string? JavaScriptFunction { get; set; } // Função de inicialização
public string? CssUrl { get; set; } // CSS opcional
public Dictionary<string, string> Assets { get; set; } = new(); // Assets adicionais
}
}

View File

@ -49,6 +49,8 @@
<script>@Html.Raw(ViewBag.ConversionPixel)</script>
}
<script src="~/js/module-loader.js"></script>
<script>
<script>
window.ModuleSystem = {
async loadModule(moduleId, targetElementId) {
@ -90,8 +92,14 @@
};
// Auto-executar quando página carrega
<!--SUBSTITUIR o script antigo do ModuleSystem por: -->
document.addEventListener('DOMContentLoaded', () => {
window.ModuleSystem.loadModules();
console.log('🚀 ModuleLoader v2.0.0 inicializado');
});
// Event listener para módulos carregados
document.addEventListener('moduleLoaded', function (event) {
console.log('✅ Módulo carregado:', event.detail);
});
</script>
</body>

View File

@ -0,0 +1,269 @@
// Dynamic Module Loader System
window.ModuleLoader = (function () {
const loadedScripts = new Set();
const loadedStyles = new Set();
const moduleInstances = new Map();
// Cache de recursos carregados
const resourceCache = new Map();
function log(message, ...args) {
console.log(`🔧 ModuleLoader: ${message}`, ...args);
}
function error(message, ...args) {
console.error(`❌ ModuleLoader: ${message}`, ...args);
}
async function loadScript(url, moduleBaseUrl) {
const fullUrl = resolveUrl(url, moduleBaseUrl);
if (loadedScripts.has(fullUrl)) {
log(`Script já carregado: ${fullUrl}`);
return true;
}
try {
log(`Carregando script: ${fullUrl}`);
const response = await fetch(fullUrl);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const scriptContent = await response.text();
// Criar script element e executar
const script = document.createElement('script');
script.textContent = scriptContent;
script.setAttribute('data-module-script', fullUrl);
document.head.appendChild(script);
loadedScripts.add(fullUrl);
log(`✅ Script carregado com sucesso: ${fullUrl}`);
return true;
} catch (err) {
error(`Falha ao carregar script ${fullUrl}:`, err);
return false;
}
}
async function loadStyle(url, moduleBaseUrl) {
const fullUrl = resolveUrl(url, moduleBaseUrl);
if (loadedStyles.has(fullUrl)) {
log(`CSS já carregado: ${fullUrl}`);
return true;
}
try {
log(`Carregando CSS: ${fullUrl}`);
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = fullUrl;
link.setAttribute('data-module-style', fullUrl);
return new Promise((resolve, reject) => {
link.onload = () => {
loadedStyles.add(fullUrl);
log(`✅ CSS carregado com sucesso: ${fullUrl}`);
resolve(true);
};
link.onerror = () => {
error(`Falha ao carregar CSS: ${fullUrl}`);
reject(false);
};
document.head.appendChild(link);
});
} catch (err) {
error(`Erro ao carregar CSS ${fullUrl}:`, err);
return false;
}
}
function resolveUrl(url, baseUrl) {
if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('//')) {
return url;
}
if (url.startsWith('/')) {
const base = new URL(baseUrl);
return `${base.protocol}//${base.host}${url}`;
}
return new URL(url, baseUrl).href;
}
function extractModuleMetadata(container) {
const metadataScript = container.querySelector('#module-metadata');
if (!metadataScript) {
log('Nenhum metadata encontrado no módulo');
return null;
}
try {
const metadata = JSON.parse(metadataScript.textContent);
log('Metadata extraído:', metadata);
return metadata;
} catch (err) {
error('Erro ao parsear metadata:', err);
return null;
}
}
function getModuleBaseUrl(moduleUrl) {
try {
const url = new URL(moduleUrl, window.location.href);
const pathParts = url.pathname.split('/');
pathParts.pop(); // Remove o último segmento (nome do endpoint)
url.pathname = pathParts.join('/');
return url.href;
} catch (err) {
error('Erro ao extrair base URL:', err);
return window.location.origin;
}
}
async function initializeModule(containerId, moduleUrl, metadata) {
const moduleBaseUrl = getModuleBaseUrl(moduleUrl);
log(`Inicializando módulo em ${containerId} com base URL: ${moduleBaseUrl}`);
try {
// Carregar CSS se especificado
if (metadata.cssUrl) {
await loadStyle(metadata.cssUrl, moduleBaseUrl);
}
// Carregar JavaScript
if (metadata.jsUrl) {
const scriptLoaded = await loadScript(metadata.jsUrl, moduleBaseUrl);
if (!scriptLoaded) {
throw new Error('Falha ao carregar script principal');
}
}
// Aguardar um momento para o script ser executado
await new Promise(resolve => setTimeout(resolve, 100));
// Chamar função de inicialização
if (metadata.jsFunction) {
const functionPath = metadata.jsFunction.split('.');
let func = window;
for (const part of functionPath) {
func = func[part];
if (!func) {
throw new Error(`Função ${metadata.jsFunction} não encontrada`);
}
}
if (typeof func === 'function') {
log(`Chamando função de inicialização: ${metadata.jsFunction}`);
const result = func(containerId);
if (result) {
moduleInstances.set(containerId, {
metadata,
moduleUrl,
moduleBaseUrl,
initialized: true
});
log(`✅ Módulo ${metadata.moduleId} inicializado com sucesso`);
return true;
}
}
}
return false;
} catch (err) {
error(`Erro ao inicializar módulo:`, err);
return false;
}
}
async function loadModule(moduleId, containerId, moduleUrl) {
log(`Carregando módulo ${moduleId} em ${containerId} de ${moduleUrl}`);
try {
// Carregar HTML do módulo
const response = await fetch(moduleUrl);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const html = await response.text();
const container = document.getElementById(containerId);
if (!container) {
throw new Error(`Container ${containerId} não encontrado`);
}
// Inserir HTML
container.innerHTML = html;
// Extrair metadata
const metadata = extractModuleMetadata(container);
if (!metadata) {
log('⚠️ Módulo sem metadata, tentando funcionar sem JavaScript');
return true;
}
// Inicializar módulo
const success = await initializeModule(containerId, moduleUrl, metadata);
if (success) {
// Disparar evento
const event = new CustomEvent('moduleLoaded', {
detail: {
moduleId: metadata.moduleId,
containerId,
moduleUrl,
metadata
}
});
document.dispatchEvent(event);
return true;
}
return false;
} catch (err) {
error(`Erro ao carregar módulo ${moduleId}:`, err);
return false;
}
}
function getModuleInfo(containerId) {
return moduleInstances.get(containerId);
}
function unloadModule(containerId) {
const info = moduleInstances.get(containerId);
if (info) {
// Limpar container
const container = document.getElementById(containerId);
if (container) {
container.innerHTML = '';
}
moduleInstances.delete(containerId);
log(`Módulo removido: ${containerId}`);
}
}
// API pública
return {
loadModule,
getModuleInfo,
unloadModule,
version: '2.0.0'
};
})();
// Compatibilidade com sistema antigo
window.ModuleSystem = {
loadModule: window.ModuleLoader.loadModule,
version: '2.0.0-compat'
};

View File

@ -1,12 +1,99 @@
using Microsoft.AspNetCore.Mvc;
using SentenceConverterModule.Services.Contracts;
namespace SentenceConverterModule.Controllers
{
public class HomeController : Controller
{
public IActionResult Index()
private readonly ISentenceConverterService _converterService;
private readonly ITextConversionApiService _apiService;
public HomeController(
ISentenceConverterService converterService,
ITextConversionApiService apiService)
{
_converterService = converterService;
_apiService = apiService;
}
public IActionResult Index(string language = "pt")
{
ViewBag.Language = language;
ViewBag.Config = _converterService.GetConfiguration(language);
// Dados para a página de teste
ViewBag.PageTitle = language switch
{
"en" => "Sentence Case Converter - Standalone Test",
"es" => "Convertidor a Mayúscula Inicial - Prueba Independiente",
_ => "Conversor para Primeira Maiúscula - Teste Standalone"
};
ViewBag.PageDescription = language switch
{
"en" => "Test the sentence case converter module independently",
"es" => "Prueba el módulo convertidor independientemente",
_ => "Teste o módulo conversor independentemente"
};
return View();
}
[HttpGet]
public async Task<IActionResult> HealthCheck()
{
try
{
var isApiHealthy = await _apiService.IsHealthyAsync();
return Json(new
{
status = "healthy",
service = "sentence-converter-module",
apiHealth = isApiHealthy,
timestamp = DateTime.UtcNow,
version = "1.0.0"
});
}
catch (Exception ex)
{
return Json(new
{
status = "unhealthy",
error = ex.Message,
timestamp = DateTime.UtcNow
});
}
}
[HttpGet]
public IActionResult TestEndpoints()
{
var baseUrl = $"{Request.Scheme}://{Request.Host}";
var endpoints = new
{
module_widget = $"{baseUrl}/modules/sentence-converter",
footer_message = $"{baseUrl}/modules/footer-message",
api_convert = $"{baseUrl}/api/converter/convert",
api_config = $"{baseUrl}/api/converter/config",
api_health = $"{baseUrl}/api/converter/health",
home_health = $"{baseUrl}/home/healthcheck"
};
return Json(endpoints);
}
public IActionResult Privacy()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View();
}
}
}

View File

@ -0,0 +1,25 @@

namespace OnlyOneAccessTemplate.Models
{
public class ModuleConfig
{
public string Id { get; set; } = string.Empty;
public string ModuleId { get; set; } = string.Empty; // Ex: "footer-message"
public string Name { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty; // URL do endpoint externo
public string RequestBy { get; set; } = string.Empty; // Identificador único
public bool IsActive { get; set; } = true;
public Dictionary<string, string> Headers { get; set; } = new();
public Dictionary<string, object> Parameters { get; set; } = new();
public int CacheMinutes { get; set; } = 5; // Cache do conteúdo
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public string? JavaScriptUrl { get; set; } // URL do arquivo JS
public string? JavaScriptFunction { get; set; } // Função de inicialização
public string? CssUrl { get; set; } // CSS opcional
public Dictionary<string, string> Assets { get; set; } = new(); // Assets adicionais
}
}

View File

@ -1,13 +0,0 @@
using SentenceConverterModule.Models;
namespace SentenceConverterModule.Services.Contracts
{
public interface IConverterService
{
string ConverterType { get; }
string ConverterName { get; }
Task<ConversionResult> ConvertAsync(ConversionRequest request);
Task<bool> ValidateInputAsync(ConversionRequest request);
ConverterConfiguration GetConfiguration(string language);
}
}

View File

@ -146,55 +146,55 @@ namespace SentenceConverterModule.Services
}
// Novo serviço que substitui o UpperLowerConversorService usando a API
public class ApiBasedSentenceConverterService : BaseConverterService
{
private readonly ITextConversionApiService _apiService;
//public class ApiBasedSentenceConverterService : BaseConverterService
//{
// private readonly ITextConversionApiService _apiService;
public override string ConverterType => "text-case-sentence";
public override string ConverterName => "Maiúsculas para minúsculas";
// public override string ConverterType => "text-case-sentence";
// public override string ConverterName => "Maiúsculas para minúsculas";
public ApiBasedSentenceConverterService(
ILogger<ApiBasedSentenceConverterService> logger,
IConfiguration configuration,
ITextConversionApiService apiService)
: base(logger, configuration)
{
_apiService = apiService;
}
// public ApiBasedSentenceConverterService(
// ILogger<ApiBasedSentenceConverterService> logger,
// IConfiguration configuration,
// ITextConversionApiService apiService)
// : base(logger, configuration)
// {
// _apiService = apiService;
// }
public override async Task<ConversionResult> ConvertAsync(ConversionRequest request)
{
try
{
var resultado = await _apiService.ConvertToSentenceCaseAsync(request.TextInput);
return new ConversionResult(true, OutputText: resultado);
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro na conversão via API");
return new ConversionResult(false, ErrorMessage: "Erro ao processar via API");
}
}
// public override async Task<ConversionResult> ConvertAsync(ConversionRequest request)
// {
// try
// {
// var resultado = await _apiService.ConvertToSentenceCaseAsync(request.TextInput);
// return new ConversionResult(true, OutputText: resultado);
// }
// catch (Exception ex)
// {
// _logger.LogError(ex, "Erro na conversão via API");
// return new ConversionResult(false, ErrorMessage: "Erro ao processar via API");
// }
// }
public override ConverterConfiguration GetConfiguration(string language)
{
var texts = GetLocalizedTexts(language);
// public override ConverterConfiguration GetConfiguration(string language)
// {
// var texts = GetLocalizedTexts(language);
texts["ConverterTitle"] = language switch
{
"en" => "Sentence Case Convert",
"es" => "Convertir a Mayúscula Inicial",
_ => "Converter para primeira maiúscula"
};
// texts["ConverterTitle"] = language switch
// {
// "en" => "Sentence Case Convert",
// "es" => "Convertir a Mayúscula Inicial",
// _ => "Converter para primeira maiúscula"
// };
return new ConverterConfiguration
{
ConverterType = "text",
OutputType = "text",
HasAdvancedOptions = false,
AllowShare = true,
LocalizedTexts = texts
};
}
}
// return new ConverterConfiguration
// {
// ConverterType = "text",
// OutputType = "text",
// HasAdvancedOptions = false,
// AllowShare = true,
// LocalizedTexts = texts
// };
// }
//}
}

View File

@ -1,8 +1,405 @@
@{
ViewData["Title"] = "Home Page";
ViewData["Title"] = "Home";
}
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>
<!-- Hero Section -->
<section class="hero-section">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-6">
<h1 class="display-4 fw-bold mb-4">
@ViewBag.Config.LocalizedTexts["ConverterTitle"]
</h1>
<p class="lead mb-4">
@ViewBag.Config.LocalizedTexts["ConverterDescription"]
</p>
<div class="d-flex gap-3">
<span class="badge bg-light text-dark fs-6">
<i class="fas fa-code me-1"></i> Standalone
</span>
<span class="badge bg-light text-dark fs-6">
<i class="fas fa-globe me-1"></i> Multi-idioma
</span>
<span class="badge bg-light text-dark fs-6">
<i class="fas fa-bolt me-1"></i> API Ready
</span>
</div>
</div>
<div class="col-lg-6 text-center">
<i class="fas fa-text-height fa-5x opacity-75"></i>
</div>
</div>
</div>
</section>
<!-- Módulo Demo -->
<section class="py-5">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-10">
<div class="text-center mb-5">
<h2>🧪 Teste do Módulo</h2>
<p class="text-muted">Este é o módulo funcionando independentemente</p>
</div>
<div class="module-demo">
<div id="sentence-converter-demo">
<!-- O módulo será carregado aqui -->
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Carregando...</span>
</div>
<p class="mt-3">Carregando módulo...</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Test Section -->
<section class="test-section">
<div class="container">
<div class="row">
<div class="col-12">
<h3 class="text-center mb-5">🔗 Endpoints Disponíveis</h3>
</div>
</div>
<div class="row" id="endpoints-container">
<!-- Endpoints serão carregados via JavaScript -->
</div>
<!-- SUBSTITUIR os cards existentes por: -->
<div class="row mt-5">
<div class="col-lg-4">
<div class="card endpoint-card h-100">
<div class="card-body">
<h5 class="card-title">
<i class="fas fa-puzzle-piece text-primary me-2"></i>
Widget Dinâmico
</h5>
<p class="card-text">Módulo carregado com o novo sistema de loading dinâmico</p>
<button class="btn btn-primary" onclick="loadWidgetManually()">
<i class="fas fa-sync me-1"></i> Recarregar Widget
</button>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card endpoint-card h-100">
<div class="card-body">
<h5 class="card-title">
<i class="fas fa-code text-success me-2"></i>
Teste de API
</h5>
<p class="card-text">Teste direto da API de conversão com resultado visual</p>
<button class="btn btn-success" onclick="testApi()">
<i class="fas fa-play me-1"></i> Testar API
</button>
<div id="api-result" class="mt-3"></div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card endpoint-card h-100">
<div class="card-body">
<h5 class="card-title">
<i class="fas fa-cogs text-info me-2"></i>
Sistema de Módulos
</h5>
<p class="card-text">Informações sobre o sistema de carregamento dinâmico</p>
<button class="btn btn-info" onclick="showSystemInfo()">
<i class="fas fa-info-circle me-1"></i> Ver Detalhes
</button>
</div>
</div>
</div>
</div>
</div>
</section>
@section Scripts {
<script>
document.addEventListener('DOMContentLoaded', function () {
// Auto-carregar o widget usando o novo sistema
loadFullWidgetDynamic();
loadEndpoints();
});
async function loadFullWidgetDynamic() {
const containerId = 'sentence-converter-demo';
const endpoint = `/modules/sentence-converter?language=@ViewBag.Language`;
console.log('🚀 Carregando widget usando sistema dinâmico...');
const success = await window.loadTestModule(containerId, endpoint);
if (success) {
console.log('✅ Widget carregado com sistema dinâmico!');
} else {
console.error('❌ Falha ao carregar widget dinamicamente');
}
}
async function loadEndpoints() {
try {
const response = await fetch('/home/testendpoints');
const endpoints = await response.json();
const container = document.getElementById('endpoints-container');
let html = '';
Object.entries(endpoints).forEach(([name, url]) => {
const displayName = name.replace(/_/g, ' ').toUpperCase();
const isModuleEndpoint = name.includes('module');
html += `
<div class="col-md-6 col-lg-4 mb-3">
<div class="card endpoint-card h-100">
<div class="card-body">
<h6 class="card-title">
${isModuleEndpoint ? '<i class="fas fa-puzzle-piece text-primary me-1"></i>' : ''}
${displayName}
</h6>
<small class="text-muted">${url}</small>
<div class="mt-2">
${isModuleEndpoint ?
`<button class="btn btn-sm btn-primary" onclick="testModuleLoad('${url}')">
<i class="fas fa-play me-1"></i> Carregar
</button>` :
`<button class="btn btn-sm btn-outline-primary" onclick="testEndpoint('${url}')">
Testar
</button>`
}
<button class="btn btn-sm btn-outline-secondary" onclick="copyUrl('${url}')">
Copiar
</button>
</div>
</div>
</div>
</div>`;
});
container.innerHTML = html;
} catch (error) {
console.error('Erro ao carregar endpoints:', error);
}
}
async function testModuleLoad(url) {
// Criar container temporário para teste
const testId = 'test-module-' + Date.now();
const modal = document.createElement('div');
modal.className = 'modal fade';
modal.innerHTML = `
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-vial me-2"></i>Teste de Carregamento do Módulo
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p><strong>URL:</strong> <code>${url}</code></p>
<div id="${testId}" class="border rounded p-3 bg-light">
<!-- Módulo será carregado aqui -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Fechar</button>
<button type="button" class="btn btn-primary" onclick="window.loadTestModule('${testId}', '${url}')">
<i class="fas fa-redo me-1"></i> Recarregar
</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
const bsModal = new bootstrap.Modal(modal);
// Limpar modal quando fechado
modal.addEventListener('hidden.bs.modal', () => {
document.body.removeChild(modal);
});
bsModal.show();
// Carregar módulo no modal
setTimeout(() => {
window.loadTestModule(testId, url);
}, 500);
}
async function testEndpoint(url) {
try {
const response = await fetch(url);
const result = await response.text();
// Abrir resultado em nova janela
const newWindow = window.open('', '_blank');
newWindow.document.write(`
<html>
<head>
<title>Resultado do Teste</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="p-4">
<div class="container">
<h3>Teste de Endpoint</h3>
<p><strong>URL:</strong> <code>${url}</code></p>
<p><strong>Status:</strong> <span class="badge bg-${response.ok ? 'success' : 'danger'}">${response.status}</span></p>
<hr>
<h5>Resposta:</h5>
<pre class="bg-light p-3 rounded"><code>${result}</code></pre>
</div>
</body>
</html>
`);
} catch (error) {
alert('Erro ao testar endpoint: ' + error.message);
}
}
async function testApi() {
const resultDiv = document.getElementById('api-result');
resultDiv.innerHTML = '<div class="spinner-border spinner-border-sm" role="status"></div> Testando API...';
try {
const formData = new FormData();
formData.append('TextInput', 'este é um teste. vamos ver se funciona!');
formData.append('Language', '@ViewBag.Language');
const response = await fetch('/api/converter/convert', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
resultDiv.innerHTML = `
<div class="alert alert-success">
<h6><i class="fas fa-check-circle me-2"></i>Sucesso!</h6>
<p class="mb-1"><strong>Entrada:</strong> este é um teste. vamos ver se funciona!</p>
<p class="mb-0"><strong>Saída:</strong> <em>${result.outputText}</em></p>
</div>`;
} else {
resultDiv.innerHTML = `
<div class="alert alert-danger">
<h6><i class="fas fa-times-circle me-2"></i>Erro</h6>
<p class="mb-0">${result.message}</p>
</div>`;
}
} catch (error) {
resultDiv.innerHTML = `
<div class="alert alert-danger">
<h6><i class="fas fa-exclamation-triangle me-2"></i>Erro de Conexão</h6>
<p class="mb-0">${error.message}</p>
</div>`;
}
}
function copyUrl(url) {
navigator.clipboard.writeText(url).then(() => {
// Feedback visual
const button = event.target;
const originalText = button.textContent;
const originalClass = button.className;
button.textContent = 'Copiado!';
button.className = button.className.replace('btn-outline-secondary', 'btn-success');
setTimeout(() => {
button.textContent = originalText;
button.className = originalClass;
}, 2000);
});
}
// Função para demonstrar carregamento manual
function loadWidgetManually() {
const endpoint = `/modules/sentence-converter?language=@ViewBag.Language`;
window.loadTestModule('sentence-converter-demo', endpoint);
}
</script>
<script>
function showSystemInfo() {
const info = {
moduleLoader: !!window.ModuleLoaderLite,
version: window.ModuleLoaderLite?.version || 'N/A',
loadedModules: document.querySelectorAll('[data-module-id]').length,
loadedScripts: document.querySelectorAll('[data-test-script]').length,
loadedStyles: document.querySelectorAll('[data-test-style]').length
};
const modal = document.createElement('div');
modal.className = 'modal fade';
modal.innerHTML = `
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-info text-white">
<h5 class="modal-title">
<i class="fas fa-info-circle me-2"></i>Informações do Sistema
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<dl class="row">
<dt class="col-sm-6">Module Loader:</dt>
<dd class="col-sm-6">
<span class="badge bg-${info.moduleLoader ? 'success' : 'danger'}">
${info.moduleLoader ? 'Ativo' : 'Inativo'}
</span>
</dd>
<dt class="col-sm-6">Versão:</dt>
<dd class="col-sm-6"><code>${info.version}</code></dd>
<dt class="col-sm-6">Módulos Carregados:</dt>
<dd class="col-sm-6"><span class="badge bg-primary">${info.loadedModules}</span></dd>
<dt class="col-sm-6">Scripts Dinâmicos:</dt>
<dd class="col-sm-6"><span class="badge bg-secondary">${info.loadedScripts}</span></dd>
<dt class="col-sm-6">Estilos Dinâmicos:</dt>
<dd class="col-sm-6"><span class="badge bg-secondary">${info.loadedStyles}</span></dd>
</dl>
<hr>
<h6>Recursos Carregados:</h6>
<div class="row">
<div class="col-12">
<small class="text-muted">Scripts:</small>
<ul class="list-unstyled small">
${Array.from(document.querySelectorAll('[data-test-script]')).map(s =>
`<li><code>${s.getAttribute('data-test-script')}</code></li>`
).join('') || '<li class="text-muted">Nenhum script dinâmico carregado</li>'}
</ul>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Fechar</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
const bsModal = new bootstrap.Modal(modal);
modal.addEventListener('hidden.bs.modal', () => {
document.body.removeChild(modal);
});
bsModal.show();
}
</script>
}

View File

@ -1,6 +1,8 @@
@{
ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>
<p>Use this page to detail your site's privacy policy.</p>
<div class="container py-5">
<h1>@ViewData["Title"]</h1>
<p>Esta é uma página de teste para o módulo Sentence Converter.</p>
<p>Use este ambiente para testar funcionalidades antes de integrar com o projeto principal.</p>
</div>

View File

@ -1,25 +1,16 @@
@model ErrorViewModel
@model string
@{
ViewData["Title"] = "Error";
}
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
<div class="container py-5">
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@if (!string.IsNullOrEmpty(Model))
{
<p>
<strong>Error details:</strong> @Model
</p>
}
</div>

View File

@ -1,4 +1,31 @@
<div class="sentence-converter-widget" data-converter-id="sentence-converter">
<!-- Metadados do Módulo -->
@* <script type="application/json" id="module-metadata">
{
"moduleId": "sentence-converter",
"jsUrl": "/js/sentence-converter-widget.js",
"jsFunction": "SentenceConverterWidget.init",
//"cssUrl": "/css/sentence-converter-widget.css",
"version": "1.0.0",
"dependencies": []
}
</script>
*@
<script type="application/json" id="module-metadata">
{
"moduleId": "sentence-converter",
"jsUrl": "/js/sentence-converter-widget.js",
"jsFunction": "SentenceConverterWidget.init",
"version": "1.0.0",
"dependencies": []
}
</script>
<!-- Widget HTML -->
<div class="sentence-converter-widget"
data-converter-id="sentence-converter"
data-module-id="sentence-converter">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-bold">@ViewBag.Config.LocalizedTexts["InputPlaceholder"]</label>
@ -38,90 +65,29 @@
<div id="conversionStatus" class="mt-3"></div>
</div>
<script>
(function () {
const widget = document.querySelector('[data-converter-id="sentence-converter"]');
if (!widget) return;
const inputText = widget.querySelector('#inputText');
const outputText = widget.querySelector('#outputText');
const convertBtn = widget.querySelector('#convertBtn');
const copyBtn = widget.querySelector('#copyBtn');
const clearBtn = widget.querySelector('#clearBtn');
const charCount = widget.querySelector('#charCount');
const status = widget.querySelector('#conversionStatus');
// Contador de caracteres
inputText.addEventListener('input', function () {
charCount.textContent = this.value.length;
});
// Converter texto
convertBtn.addEventListener('click', async function () {
const text = inputText.value.trim();
if (!text) {
showStatus('Por favor, digite algum texto.', 'warning');
return;
}
convertBtn.disabled = true;
convertBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Convertendo...';
try {
const formData = new FormData();
formData.append('TextInput', text);
formData.append('Language', '@ViewBag.Language');
const response = await fetch('/api/converter/convert', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
outputText.value = result.outputText;
copyBtn.disabled = false;
showStatus('Conversão realizada com sucesso!', 'success');
} else {
showStatus('Erro: ' + result.message, 'danger');
}
} catch (error) {
showStatus('Erro de conexão. Tente novamente.', 'danger');
} finally {
convertBtn.disabled = false;
convertBtn.innerHTML = '<i class="fas fa-exchange-alt me-2"></i>@ViewBag.Config.LocalizedTexts["ConvertButton"]';
}
});
// Copiar resultado
copyBtn.addEventListener('click', async function () {
try {
await navigator.clipboard.writeText(outputText.value);
showStatus('Texto copiado para a área de transferência!', 'success');
} catch (error) {
showStatus('Erro ao copiar texto.', 'danger');
}
});
// Limpar campos
clearBtn.addEventListener('click', function () {
inputText.value = '';
outputText.value = '';
copyBtn.disabled = true;
charCount.textContent = '0';
status.innerHTML = '';
});
function showStatus(message, type) {
status.innerHTML = `<div class="alert alert-${type} alert-dismissible fade show" role="alert">
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>`;
setTimeout(() => {
status.innerHTML = '';
}, 5000);
<!-- Configuração para o JavaScript -->
<script type="application/json" id="widget-config">
{
"language": "@ViewBag.Language",
"convertButtonText": "@ViewBag.Config.LocalizedTexts["ConvertButton"]",
"labels": {
"inputPlaceholder": "@ViewBag.Config.LocalizedTexts["InputPlaceholder"]",
"outputLabel": "@ViewBag.Config.LocalizedTexts["OutputLabel"]",
"convertButton": "@ViewBag.Config.LocalizedTexts["ConvertButton"]",
"copyButton": "@ViewBag.Config.LocalizedTexts["CopyButton"]",
"clearButton": "@ViewBag.Config.LocalizedTexts["ClearButton"]"
},
"messages": {
"enterText": "Por favor, digite algum texto.",
"converting": "Convertendo...",
"success": "Conversão realizada com sucesso!",
"copied": "Texto copiado para a área de transferência!",
"connectionError": "Erro de conexão. Tente novamente.",
"copyError": "Erro ao copiar texto."
},
"endpoints": {
"convert": "/api/converter/convert",
"health": "/api/converter/health"
}
})();
}
</script>

View File

@ -1,49 +1,244 @@
<!DOCTYPE html>
<html lang="en">
<html lang="@ViewBag.Language">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - UpperFirstLetter</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/UpperFirstLetter.styles.css" asp-append-version="true" />
<title>@(ViewBag.PageTitle ?? "Sentence Converter Module")</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
.hero-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 60px 0;
}
.test-section {
background-color: #f8f9fa;
padding: 40px 0;
}
.endpoint-card {
transition: transform 0.2s;
border: none;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.endpoint-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
margin-right: 8px;
}
.status-healthy {
background-color: #28a745;
}
.status-unhealthy {
background-color: #dc3545;
}
.status-unknown {
background-color: #6c757d;
}
.module-demo {
border: 2px dashed #dee2e6;
border-radius: 8px;
padding: 20px;
background-color: white;
}
</style>
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container-fluid">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">UpperFirstLetter</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</li>
</ul>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="/">
<i class="fas fa-text-height me-2"></i>
Sentence Converter
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="/">Home</a>
</li>
</ul>
<div class="navbar-nav">
<div class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
@(ViewBag.Language?.ToString().ToUpper() ?? "PT")
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/?language=pt">Português</a></li>
<li><a class="dropdown-item" href="/?language=en">English</a></li>
<li><a class="dropdown-item" href="/?language=es">Español</a></li>
</ul>
</div>
<span class="navbar-text ms-3">
<span class="status-indicator" id="health-indicator"></span>
<span id="health-text">Verificando...</span>
</span>
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
</div>
</nav>
<footer class="border-top footer text-muted">
<main>
@RenderBody()
</main>
<footer class="bg-dark text-light py-4 mt-5">
<div class="container">
&copy; 2025 - UpperFirstLetter - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
<div class="row">
<div class="col-md-6">
<h6>Sentence Converter Module</h6>
<p class="mb-0">Módulo standalone para conversão de texto</p>
</div>
<div class="col-md-6 text-md-end">
<small class="text-muted">Versão: 1.0.0 | Port: @Context.Request.Host.Port</small>
</div>
</div>
</div>
</footer>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
<!-- SUBSTITUIR a seção de scripts por: -->
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- Module Loader Lite para testes standalone -->
<script src="~/js/module-loader-lite.js"></script>
<!-- Sistema de carregamento para ambiente de teste -->
<script>
document.addEventListener('DOMContentLoaded', () => {
console.log('🚀 ModuleLoaderLite v1.0.0 inicializado para testes');
});
// Event listener para módulos carregados localmente
document.addEventListener('moduleLoadedLocal', function (event) {
console.log('✅ Módulo local carregado:', event.detail);
// Mostrar feedback visual
showModuleLoadedFeedback(event.detail);
});
function showModuleLoadedFeedback(detail) {
const toast = document.createElement('div');
toast.className = 'toast position-fixed top-0 end-0 m-3';
toast.style.zIndex = '9999';
toast.innerHTML = `
<div class="toast-header bg-success text-white">
<i class="fas fa-check-circle me-2"></i>
<strong class="me-auto">Módulo Carregado</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast"></button>
</div>
<div class="toast-body">
<strong>${detail.moduleId}</strong> inicializado com sucesso!<br>
<small class="text-muted">Modo: ${detail.mode}</small>
</div>
`;
document.body.appendChild(toast);
const bsToast = new bootstrap.Toast(toast);
bsToast.show();
// Remove o toast após ser ocultado
toast.addEventListener('hidden.bs.toast', () => {
document.body.removeChild(toast);
});
}
// Função global para carregar módulos em testes
window.loadTestModule = async function (containerId, endpoint) {
console.log(`🧪 Carregando módulo de teste: ${endpoint} -> ${containerId}`);
const container = document.getElementById(containerId);
if (container) {
// Mostrar loading
container.innerHTML = `
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Carregando...</span>
</div>
<p class="mt-3">Carregando módulo de teste...</p>
<small class="text-muted">Endpoint: ${endpoint}</small>
</div>
`;
}
try {
const success = await window.ModuleLoaderLite.loadModule(containerId, endpoint);
if (!success) {
throw new Error('Falha ao inicializar módulo');
}
return true;
} catch (error) {
console.error('❌ Erro ao carregar módulo de teste:', error);
if (container) {
container.innerHTML = `
<div class="alert alert-danger">
<h6><i class="fas fa-exclamation-triangle me-2"></i>Erro ao Carregar Módulo</h6>
<p class="mb-2"><strong>Endpoint:</strong> ${endpoint}</p>
<p class="mb-2"><strong>Erro:</strong> ${error.message}</p>
<button class="btn btn-sm btn-outline-danger" onclick="loadTestModule('${containerId}', '${endpoint}')">
<i class="fas fa-redo me-1"></i> Tentar Novamente
</button>
</div>
`;
}
return false;
}
};
</script>
<!-- Health Check Script (manter existente) -->
<script>
document.addEventListener('DOMContentLoaded', function () {
checkHealth();
setInterval(checkHealth, 30000);
});
async function checkHealth() {
const indicator = document.getElementById('health-indicator');
const text = document.getElementById('health-text');
try {
const response = await fetch('/home/healthcheck');
const data = await response.json();
if (data.status === 'healthy') {
indicator.className = 'status-indicator status-healthy';
text.textContent = 'Online';
} else {
indicator.className = 'status-indicator status-unhealthy';
text.textContent = 'Erro';
}
} catch (error) {
indicator.className = 'status-indicator status-unknown';
text.textContent = 'Offline';
}
}
</script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>

View File

@ -0,0 +1,189 @@
// Module Loader Lite - Versão simplificada para testes standalone
window.ModuleLoaderLite = (function () {
function log(message, ...args) {
console.log(`🧪 ModuleLoaderLite: ${message}`, ...args);
}
function error(message, ...args) {
console.error(`❌ ModuleLoaderLite: ${message}`, ...args);
}
function extractModuleMetadata(container) {
const metadataScript = container.querySelector('#module-metadata');
if (!metadataScript) {
log('Nenhum metadata encontrado no módulo');
return null;
}
try {
const metadata = JSON.parse(metadataScript.textContent);
log('Metadata extraído:', metadata);
return metadata;
} catch (err) {
error('Erro ao parsear metadata:', err);
return null;
}
}
async function loadLocalScript(url) {
try {
log(`Carregando script local: ${url}`);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const scriptContent = await response.text();
// Criar script element e executar
const script = document.createElement('script');
script.textContent = scriptContent;
script.setAttribute('data-test-script', url);
document.head.appendChild(script);
log(`✅ Script local carregado: ${url}`);
return true;
} catch (err) {
error(`Falha ao carregar script ${url}:`, err);
return false;
}
}
async function loadLocalStyle(url) {
try {
log(`Carregando CSS local: ${url}`);
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = url;
link.setAttribute('data-test-style', url);
return new Promise((resolve, reject) => {
link.onload = () => {
log(`✅ CSS local carregado: ${url}`);
resolve(true);
};
link.onerror = () => {
error(`Falha ao carregar CSS: ${url}`);
reject(false);
};
document.head.appendChild(link);
});
} catch (err) {
error(`Erro ao carregar CSS ${url}:`, err);
return false;
}
}
async function initializeLocalModule(containerId) {
log(`Inicializando módulo local em: ${containerId}`);
const container = document.getElementById(containerId);
if (!container) {
error(`Container não encontrado: ${containerId}`);
return false;
}
// Extrair metadata
const metadata = extractModuleMetadata(container);
if (!metadata) {
error('Metadata não encontrado, não é possível inicializar');
return false;
}
try {
// Carregar CSS se especificado
if (metadata.cssUrl) {
await loadLocalStyle(metadata.cssUrl);
}
// Carregar JavaScript
if (metadata.jsUrl) {
const scriptLoaded = await loadLocalScript(metadata.jsUrl);
if (!scriptLoaded) {
throw new Error('Falha ao carregar script principal');
}
}
// Aguardar execução do script
await new Promise(resolve => setTimeout(resolve, 200));
// Chamar função de inicialização
if (metadata.jsFunction) {
const functionPath = metadata.jsFunction.split('.');
let func = window;
for (const part of functionPath) {
func = func[part];
if (!func) {
throw new Error(`Função ${metadata.jsFunction} não encontrada`);
}
}
if (typeof func === 'function') {
log(`Chamando função: ${metadata.jsFunction}`);
const result = func(containerId);
if (result) {
log(`✅ Módulo ${metadata.moduleId} inicializado localmente`);
// Disparar evento
const event = new CustomEvent('moduleLoadedLocal', {
detail: {
moduleId: metadata.moduleId,
containerId,
metadata,
mode: 'standalone'
}
});
document.dispatchEvent(event);
return true;
}
}
}
return false;
} catch (err) {
error(`Erro ao inicializar módulo local:`, err);
return false;
}
}
async function loadModuleLocally(containerId, moduleEndpoint) {
log(`Carregando módulo local em ${containerId} de ${moduleEndpoint}`);
try {
const response = await fetch(moduleEndpoint);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const html = await response.text();
const container = document.getElementById(containerId);
if (!container) {
throw new Error(`Container ${containerId} não encontrado`);
}
// Inserir HTML
container.innerHTML = html;
// Inicializar
return await initializeLocalModule(containerId);
} catch (err) {
error(`Erro ao carregar módulo local:`, err);
return false;
}
}
// API pública
return {
loadModule: loadModuleLocally,
initializeModule: initializeLocalModule,
version: '1.0.0-lite'
};
})();

View File

@ -0,0 +1,196 @@
// Sentence Converter Widget JavaScript
window.SentenceConverterWidget = (function () {
function initializeWidget(containerId) {
console.log('🚀 Inicializando SentenceConverterWidget em:', containerId);
const container = document.getElementById(containerId);
if (!container) {
console.error('❌ Container não encontrado:', containerId);
return false;
}
const widget = container.querySelector('[data-converter-id="sentence-converter"]');
if (!widget) {
console.error('❌ Widget não encontrado no container');
return false;
}
// Buscar configuração
const configScript = widget.querySelector('#widget-config');
let config = {};
if (configScript) {
try {
config = JSON.parse(configScript.textContent);
console.log('✅ Configuração carregada:', config);
} catch (e) {
console.warn('⚠️ Erro ao parsear configuração, usando fallback');
config = getFallbackConfig();
}
} else {
console.warn('⚠️ Configuração não encontrada, usando fallback');
config = getFallbackConfig();
}
// Elementos do widget
const elements = {
inputText: widget.querySelector('#inputText'),
outputText: widget.querySelector('#outputText'),
convertBtn: widget.querySelector('#convertBtn'),
copyBtn: widget.querySelector('#copyBtn'),
clearBtn: widget.querySelector('#clearBtn'),
charCount: widget.querySelector('#charCount'),
status: widget.querySelector('#conversionStatus')
};
// Verificar se todos os elementos existem
const missingElements = Object.entries(elements)
.filter(([key, element]) => !element)
.map(([key]) => key);
if (missingElements.length > 0) {
console.error('❌ Elementos não encontrados:', missingElements);
return false;
}
console.log('✅ Todos os elementos encontrados');
// Event Listeners
setupEventListeners(elements, config);
console.log('✅ SentenceConverterWidget inicializado com sucesso!');
return true;
}
function setupEventListeners(elements, config) {
const { inputText, outputText, convertBtn, copyBtn, clearBtn, charCount, status } = elements;
// Contador de caracteres
inputText.addEventListener('input', function () {
charCount.textContent = this.value.length;
console.log('📝 Caracteres digitados:', this.value.length);
});
// Converter texto
convertBtn.addEventListener('click', async function () {
console.log('🔄 Iniciando conversão...');
const text = inputText.value.trim();
if (!text) {
showStatus(status, config.messages.enterText || 'Por favor, digite algum texto.', 'warning');
return;
}
// UI Loading state
convertBtn.disabled = true;
const originalText = convertBtn.innerHTML;
convertBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>' + (config.messages.converting || 'Convertendo...');
try {
const formData = new FormData();
formData.append('TextInput', text);
formData.append('Language', config.language || 'pt');
console.log('📤 Enviando requisição para /api/converter/convert');
const response = await fetch('/api/converter/convert', {
method: 'POST',
body: formData
});
const result = await response.json();
console.log('📥 Resposta recebida:', result);
if (result.success) {
outputText.value = result.outputText;
copyBtn.disabled = false;
showStatus(status, config.messages.success || 'Conversão realizada com sucesso!', 'success');
console.log('✅ Conversão bem-sucedida');
} else {
showStatus(status, 'Erro: ' + result.message, 'danger');
console.error('❌ Erro na conversão:', result.message);
}
} catch (error) {
showStatus(status, config.messages.connectionError || 'Erro de conexão. Tente novamente.', 'danger');
console.error('❌ Erro de conexão:', error);
} finally {
convertBtn.disabled = false;
convertBtn.innerHTML = originalText;
}
});
// Copiar resultado
copyBtn.addEventListener('click', async function () {
try {
await navigator.clipboard.writeText(outputText.value);
showStatus(status, config.messages.copied || 'Texto copiado para a área de transferência!', 'success');
console.log('📋 Texto copiado com sucesso');
} catch (error) {
showStatus(status, config.messages.copyError || 'Erro ao copiar texto.', 'danger');
console.error('❌ Erro ao copiar:', error);
}
});
// Limpar campos
clearBtn.addEventListener('click', function () {
inputText.value = '';
outputText.value = '';
copyBtn.disabled = true;
charCount.textContent = '0';
status.innerHTML = '';
console.log('🧹 Campos limpos');
});
console.log('🎯 Event listeners configurados');
}
function showStatus(statusElement, message, type) {
statusElement.innerHTML = `
<div class="alert alert-${type} alert-dismissible fade show" role="alert">
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>`;
setTimeout(() => {
statusElement.innerHTML = '';
}, 5000);
}
function getFallbackConfig() {
return {
language: 'pt',
convertButtonText: 'Converter Texto',
messages: {
enterText: 'Por favor, digite algum texto.',
converting: 'Convertendo...',
success: 'Conversão realizada com sucesso!',
copied: 'Texto copiado para a área de transferência!',
connectionError: 'Erro de conexão. Tente novamente.',
copyError: 'Erro ao copiar texto.'
}
};
}
// API pública
return {
init: initializeWidget,
version: '1.0.0'
};
})();
// Auto-inicializar se encontrar widgets na página
// Auto-inicializar se encontrar widgets na página (para uso standalone)
document.addEventListener('DOMContentLoaded', function () {
// Só auto-inicializar se não estivermos em um sistema de módulos
if (!window.ModuleLoader && !window.ModuleSystem) {
const widgets = document.querySelectorAll('[data-converter-id="sentence-converter"]');
widgets.forEach((widget, index) => {
const containerId = widget.closest('[id]')?.id || `widget-container-${index}`;
if (!widget.closest('[id]')) {
widget.parentElement.id = containerId;
}
window.SentenceConverterWidget.init(containerId);
});
}
});