feat(F-099): completed feature

This commit is contained in:
chattie
2026-08-21 07:29:07 +02:00
parent 3f1d08382f
commit 5177a851aa
41 changed files with 922 additions and 131 deletions

View File

@@ -9,6 +9,7 @@ const TABS = [
{ id: 'social', label: 'Redes sociales', icon: '🌐' },
{ id: 'footer', label: 'Footer', icon: '📄' },
{ id: 'ai', label: 'IA para SEO', icon: '✨' },
{ id: 'smtp', label: 'SMTP / Email', icon: '✉️' },
] as const;
type TabId = (typeof TABS)[number]['id'];
@@ -32,8 +33,12 @@ export default function SettingsPage() {
if (!form) return;
setSaving(true); setErr(''); setMsg('');
try {
const { aiApiKey, ...settingsWithoutKey } = form;
const updated = await settingsApi.update(aiApiKey ? form : settingsWithoutKey);
const { aiApiKey, smtpPass, ...settingsWithoutSecrets } = form;
const updated = await settingsApi.update({
...settingsWithoutSecrets,
...(aiApiKey ? { aiApiKey } : {}),
...(smtpPass ? { smtpPass } : {}),
});
setData(updated); setForm(updated);
setMsg('Cambios guardados correctamente');
setTimeout(() => setMsg(''), 4000);
@@ -44,7 +49,7 @@ export default function SettingsPage() {
}
};
const field = (key: Exclude<keyof FormData, 'aiApiKeyConfigured'>, label: string, opts?: { type?: string; placeholder?: string; rows?: number; hint?: string }) => (
const field = (key: Exclude<keyof FormData, 'aiApiKeyConfigured' | 'smtpPassConfigured' | 'smtpSecure'>, label: string, opts?: { type?: string; placeholder?: string; rows?: number; hint?: string }) => (
<div key={key}>
<label className="block text-sm font-medium text-gray-700 mb-1">{label}</label>
{opts?.rows ? (
@@ -144,6 +149,27 @@ export default function SettingsPage() {
{field('aiApiKey', 'API key', { type: 'password', placeholder: form?.aiApiKeyConfigured ? 'API key configurada (escribe para reemplazar)' : 'sk-...' })}
{field('aiSeoTitlePrompt', 'Prompt para Título SEO', { rows: 4, hint: 'Usa {{name}}, {{description}} y {{brand}} como variables.' })}
{field('aiSeoDescriptionPrompt', 'Prompt para Descripción SEO (Google)', { rows: 5, hint: 'Usa {{name}}, {{description}} y {{brand}} como variables.' })}
{field('aiProductDescriptionPrompt', 'Prompt para Descripción del producto', { rows: 5, hint: 'Se usa solo cuando la descripción normal está vacía. Variables: {{name}}, {{description}} y {{brand}}.' })}
</div>
</>
)}
{tab === 'smtp' && (
<>
<div className="px-6 py-4 bg-gray-50 border-b border-gray-200">
<h2 className="text-base font-semibold text-gray-800">Correo SMTP</h2>
<p className="text-xs text-gray-400 mt-0.5">Se usa para enviar enlaces de recuperación de contraseña.</p>
</div>
<div className="p-6 space-y-5">
{field('smtpHost', 'Servidor SMTP', { placeholder: 'ssl0.ovh.net' })}
{field('smtpPort', 'Puerto', { type: 'number', placeholder: '465' })}
<label className="flex items-center gap-2 text-sm text-gray-700">
<input type="checkbox" checked={form?.smtpSecure ?? true} onChange={e => setForm(f => f ? { ...f, smtpSecure: e.target.checked } : f)} />
Conexión segura SSL/TLS
</label>
{field('smtpUser', 'Usuario / cuenta de correo', { type: 'email', placeholder: 'info@mercadodevida.es' })}
{field('smtpPass', 'Contraseña SMTP', { type: 'password', placeholder: form?.smtpPassConfigured ? 'Contraseña configurada (escribe para reemplazar)' : 'Contraseña del buzón' })}
{field('smtpFrom', 'Remitente', { type: 'email', placeholder: 'info@mercadodevida.es' })}
</div>
</>
)}

View File

@@ -17,6 +17,16 @@ export async function GET(req: NextRequest) {
const backendRes = await fetch(`${API}/${path}`, {
headers: { Cookie: cookies },
});
if (path === 'admin/logs/stream') {
return new Response(backendRes.body, {
status: backendRes.status,
headers: {
'Content-Type': backendRes.headers.get('content-type') ?? 'text/event-stream',
'Cache-Control': backendRes.headers.get('cache-control') ?? 'no-cache',
Connection: 'keep-alive',
},
});
}
const data = await backendRes.json().catch(() => null);
const resp = NextResponse.json(data ?? { error: 'Bad response' }, { status: backendRes.status });
return resp;