- 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
136 lines
5.1 KiB
TypeScript
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>
|
|
);
|
|
}
|