gohorsejobs/frontend/src/app/dashboard/admin/email-templates/[slug]/page.tsx
Tiago Yamamoto 841b1d780c feat: Email System, Avatar Upload, Email Templates UI, and Public Job Posting
- Backend: Email producer (LavinMQ), EmailService interface
- Backend: CRUD API for email_templates and email_settings
- Backend: avatar_url field in users table + UpdateMyProfile support
- Backend: StorageService for pre-signed URLs
- NestJS: Email consumer with Nodemailer and Handlebars
- Frontend: Email Templates admin pages (list/edit)
- Frontend: Updated profileApi.uploadAvatar with pre-signed URL flow
- Frontend: New /post-job public page (company registration + job creation wizard)
- Migrations: 027_create_email_system.sql, 028_add_avatar_url_to_users.sql
2025-12-26 12:21:34 -03:00

136 lines
5.1 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { useRouter, useParams } from "next/navigation";
import { toast } from "sonner";
import { emailTemplatesApi, EmailTemplate } from "@/lib/api";
export default function EditEmailTemplatePage() {
const router = useRouter();
const params = useParams();
const slug = params.slug as string;
const [template, setTemplate] = useState<EmailTemplate | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
useEffect(() => {
fetchTemplate();
}, [slug]);
const fetchTemplate = async () => {
try {
const data = await emailTemplatesApi.get(slug);
setTemplate(data);
} catch (err: any) {
toast.error(err.message || "Failed to load template");
} finally {
setLoading(false);
}
};
const handleSave = async () => {
if (!template) return;
setSaving(true);
try {
await emailTemplatesApi.update(slug, {
subject: template.subject,
body_html: template.body_html,
variables: template.variables,
});
toast.success("Template saved!");
} catch (err: any) {
toast.error(err.message || "Failed to save template");
} finally {
setSaving(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
);
}
if (!template) {
return (
<div className="max-w-3xl mx-auto p-6 text-center">
<p className="text-red-500">Template not found</p>
<button onClick={() => router.push("/dashboard/admin/email-templates")} className="mt-4 text-blue-500">
Back to list
</button>
</div>
);
}
return (
<div className="max-w-4xl mx-auto p-6">
<button
onClick={() => router.push("/dashboard/admin/email-templates")}
className="mb-4 text-sm text-gray-500 hover:text-gray-700 flex items-center gap-1"
>
Back to Templates
</button>
<h1 className="text-2xl font-bold mb-6">Edit Template: {slug}</h1>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 space-y-6">
<div>
<label className="block text-sm font-medium mb-1">Subject</label>
<input
type="text"
value={template.subject}
onChange={(e) => setTemplate({ ...template, subject: e.target.value })}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Body (HTML)</label>
<textarea
value={template.body_html}
onChange={(e) => setTemplate({ ...template, body_html: e.target.value })}
rows={15}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary font-mono text-sm"
/>
<p className="text-xs text-gray-500 mt-1">
Use {"{{variableName}}"} for dynamic content. Variables: {template.variables.join(", ")}
</p>
</div>
<div>
<label className="block text-sm font-medium mb-1">Variables (comma-separated)</label>
<input
type="text"
value={(template.variables || []).join(", ")}
onChange={(e) =>
setTemplate({
...template,
variables: e.target.value.split(",").map((v) => v.trim()).filter(Boolean),
})
}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary"
/>
</div>
<div className="flex gap-3">
<button
onClick={handleSave}
disabled={saving}
className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition disabled:opacity-50"
>
{saving ? "Saving..." : "Save Changes"}
</button>
<button
onClick={() => router.push("/dashboard/admin/email-templates")}
className="px-6 py-2 bg-gray-200 dark:bg-gray-600 rounded-lg hover:bg-gray-300 transition"
>
Cancel
</button>
</div>
</div>
</div>
);
}