diff --git a/app/admin/email/manage/page.tsx b/app/admin/email/manage/page.tsx new file mode 100644 index 0000000..b312210 --- /dev/null +++ b/app/admin/email/manage/page.tsx @@ -0,0 +1,346 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { useAuth } from "@/context/AuthContext"; +import { + fetchEmailRequests, + createEmailRequest, + updateEmailRequest, + deleteEmailRequest, + type EmailRequest, + type CreateEmailRequestDto, + type UpdateEmailRequestDto, +} from "@/lib/api/emailRequests"; +import { Pencil, Trash2, Plus, X } from "lucide-react"; +import { formatDate } from "@/lib/dateFormattingUtility"; + +type EmailStatusTab = "ALL" | "PENDING" | "SENT" | "FAILED" | "OTHER"; + +export default function AdminEmailPage() { + const { token } = useAuth(); + const [emails, setEmails] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [showModal, setShowModal] = useState(false); + const [modalMode, setModalMode] = useState<"create" | "edit">("create"); + const [selectedEmail, setSelectedEmail] = useState(null); + const [formData, setFormData] = useState({ + email: "", + status: "", + }); + const [submitting, setSubmitting] = useState(false); + + const [statusTab, setStatusTab] = useState("ALL"); + + const loadEmails = async () => { + if (!token) return; + + try { + setLoading(true); + const data = await fetchEmailRequests(token); + setEmails(data.data); + setError(null); + } catch (e: any) { + console.error(e); + setError(e?.message ?? "Failed to load email requests"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadEmails(); + }, [token]); + + const normalizeStatus = (s: unknown) => String(s ?? "").trim().toUpperCase(); + + const counts = useMemo(() => { + const c = { ALL: 0, PENDING: 0, SENT: 0, FAILED: 0, OTHER: 0 } as Record< + EmailStatusTab, + number + >; + + c.ALL = emails.length; + + for (const e of emails) { + const st = normalizeStatus(e.status); + if (st === "PENDING") c.PENDING += 1; + else if (st === "SENT") c.SENT += 1; + else if (st === "FAILED") c.FAILED += 1; + else c.OTHER += 1; + } + + return c; + }, [emails]); + + const filteredEmails = useMemo(() => { + if (statusTab === "ALL") return emails; + + return emails.filter((e) => { + const st = normalizeStatus(e.status); + if (statusTab === "OTHER") { + return st !== "PENDING" && st !== "SENT" && st !== "FAILED"; + } + return st === statusTab; + }); + }, [emails, statusTab]); + + const tabs: { key: EmailStatusTab; label: string }[] = [ + { key: "ALL", label: "All" }, + { key: "PENDING", label: "Pending" }, + { key: "SENT", label: "Sent" }, + { key: "FAILED", label: "Failed" }, + { key: "OTHER", label: "Other" }, + ]; + + const handleCreate = () => { + setModalMode("create"); + setFormData({ email: "", status: "" }); + setSelectedEmail(null); + setShowModal(true); + }; + + const handleEdit = (email: EmailRequest) => { + setModalMode("edit"); + setFormData({ email: email.email, status: email.status }); + setSelectedEmail(email); + setShowModal(true); + }; + + const handleDelete = async (id: number) => { + if (!token) return; + if (!confirm("Are you sure you want to delete this email request?")) return; + + try { + await deleteEmailRequest(token, id); + await loadEmails(); + } catch (e: any) { + alert(e?.message ?? "Failed to delete email request"); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!token) return; + + try { + setSubmitting(true); + if (modalMode === "create") { + await createEmailRequest(token, formData); + } else if (selectedEmail) { + const updateData: UpdateEmailRequestDto = {}; + if (formData.email !== selectedEmail.email) updateData.email = formData.email; + if (formData.status !== selectedEmail.status) updateData.status = formData.status; + await updateEmailRequest(token, selectedEmail.id, updateData); + } + setShowModal(false); + await loadEmails(); + } catch (e: any) { + alert(e?.message ?? "Failed to save email request"); + } finally { + setSubmitting(false); + } + }; + + if (!token) { + return ( +
+ You must be logged in to view this page. +
+ ); + } + + return ( +
+ {/* Header */} +
+
+

Emails

+

+ Manage Email and their statuses. +

+
+ +
+ + {/* Status Tabs */} +
+ {tabs.map((t) => { + const active = statusTab === t.key; + return ( + + ); + })} +
+ + {/* Error */} + {error && ( +
+ {error} +
+ )} + + {/* Table */} + {loading ? ( +
Loading…
+ ) : ( +
+ + + + + + + + + + + + + {filteredEmails.map((email) => ( + + + + + + + + + ))} + + {filteredEmails.length === 0 && ( + + + + )} + +
IDEmailStatusCreated AtUpdated AtActions
{email.id}{email.recipient} + + {email.status} + + + {new Date(email.createdAt).toLocaleString()} + + {email.updatedAt ? formatDate(email.updatedAt) : "—"} + +
+ + +
+
+ No email requests found for{" "} + {tabs.find((t) => t.key === statusTab)?.label}. +
+
+ )} + + {/* Modal */} + {showModal && ( +
+
+
+

+ {modalMode === "create" ? "Create Email Request" : "Edit Email Request"} +

+ +
+ +
+
+ + setFormData({ ...formData, email: e.target.value })} + className="w-full rounded-md border border-zinc-800 bg-zinc-900 px-3 py-2 text-sm text-zinc-100 placeholder-zinc-500 focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500" + placeholder="user@example.com" + required + /> +
+ +
+ + setFormData({ ...formData, status: e.target.value })} + className="w-full rounded-md border border-zinc-800 bg-zinc-900 px-3 py-2 text-sm text-zinc-100 placeholder-zinc-500 focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500" + placeholder="pending" + /> +
+ +
+ + +
+
+
+
+ )} +
+ ); +} diff --git a/app/admin/email/page.tsx b/app/admin/email/page.tsx index c1f19fa..4ba1be8 100644 --- a/app/admin/email/page.tsx +++ b/app/admin/email/page.tsx @@ -12,7 +12,7 @@ import { type UpdateEmailRequestDto, } from "@/lib/api/emailRequests"; import { Pencil, Trash2, Plus, X } from "lucide-react"; -import { formatDate } from "@/lib/api/dateFormattingUtility"; +import { formatDate } from "@/lib/dateFormattingUtility"; export default function AdminEmailPage() { const { token } = useAuth(); const [emails, setEmails] = useState([]); @@ -110,7 +110,7 @@ export default function AdminEmailPage() {

- Email Requests + Processed E-mail

Manage email requests and their statuses. @@ -155,7 +155,7 @@ export default function AdminEmailPage() { className="border-t border-zinc-800/70 hover:bg-zinc-900/60" > {email.id} - {email.email} + {email.recipient} {email.status} diff --git a/components/AdminLeftNavigation.tsx b/components/AdminLeftNavigation.tsx index c229cd7..c8df0cb 100644 --- a/components/AdminLeftNavigation.tsx +++ b/components/AdminLeftNavigation.tsx @@ -11,6 +11,7 @@ import { Settings, LucideMail, } from "lucide-react"; +import Link from "next/link"; type AdminLeftNavigationProps = { collapsed: boolean; @@ -59,8 +60,8 @@ const navItems = [ icon: , }, { - label: "List Emails", - href: "/admin/email", + label: "Manage Emails", + href: "/admin/email/manage", icon: , }, { @@ -97,7 +98,7 @@ export default function AdminLeftNavigation({ {!collapsed && (

- Battl Builders + Battl Builders

Admin Command

diff --git a/components/ui/RichTextEditor.tsx b/components/ui/RichTextEditor.tsx index c437c9a..5610aa8 100644 --- a/components/ui/RichTextEditor.tsx +++ b/components/ui/RichTextEditor.tsx @@ -1,5 +1,6 @@ "use client"; - +import QuillImageResize from "quill-image-resize-module"; +Quill.register("modules/imageResize", QuillImageResize); import dynamic from "next/dynamic"; import type React from "react"; diff --git a/lib/api/emailRequests.ts b/lib/api/emailRequests.ts index 4e6e325..fe4db01 100644 --- a/lib/api/emailRequests.ts +++ b/lib/api/emailRequests.ts @@ -1,5 +1,6 @@ export interface EmailRequest { id: number; + recipient: string; email: string; status: string; createdAt: string; diff --git a/lib/api/dateFormattingUtility.ts b/lib/dateFormattingUtility.ts similarity index 100% rename from lib/api/dateFormattingUtility.ts rename to lib/dateFormattingUtility.ts diff --git a/package-lock.json b/package-lock.json index 4647936..b9a4ede 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "lucide-react": "^0.555.0", "next": "14.2.3", "quill": "^2.0.3", + "quill-image-resize-module": "^3.0.0", "react": "18.3.1", "react-dom": "18.3.1", "react-quill": "^2.0.0" @@ -4712,6 +4713,68 @@ "node": ">= 12.0.0" } }, + "node_modules/quill-image-resize-module": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quill-image-resize-module/-/quill-image-resize-module-3.0.0.tgz", + "integrity": "sha512-1TZBnUxU/WIx5dPyVjQ9yN7C6mLZSp04HyWBEMqT320DIq4MW4JgzlOPDZX5ZpBM3bU6sacU4kTLUc8VgYQZYw==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.4", + "quill": "^1.2.2", + "raw-loader": "^0.5.1" + } + }, + "node_modules/quill-image-resize-module/node_modules/eventemitter3": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", + "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==", + "license": "MIT" + }, + "node_modules/quill-image-resize-module/node_modules/fast-diff": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", + "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==", + "license": "Apache-2.0" + }, + "node_modules/quill-image-resize-module/node_modules/parchment": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz", + "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==", + "license": "BSD-3-Clause" + }, + "node_modules/quill-image-resize-module/node_modules/quill": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz", + "integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==", + "license": "BSD-3-Clause", + "dependencies": { + "clone": "^2.1.1", + "deep-equal": "^1.0.1", + "eventemitter3": "^2.0.3", + "extend": "^3.0.2", + "parchment": "^1.1.4", + "quill-delta": "^3.6.2" + } + }, + "node_modules/quill-image-resize-module/node_modules/quill-delta": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz", + "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==", + "license": "MIT", + "dependencies": { + "deep-equal": "^1.0.1", + "extend": "^3.0.2", + "fast-diff": "1.1.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/raw-loader": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-0.5.1.tgz", + "integrity": "sha512-sf7oGoLuaYAScB4VGr0tzetsYlS8EJH6qnTCfQ/WVEa89hALQ4RQfCKt5xCyPQKPDUbVUAIP1QsxAwfAjlDp7Q==" + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", diff --git a/package.json b/package.json index 0a5ce72..130fb54 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "lucide-react": "^0.555.0", "next": "14.2.3", "quill": "^2.0.3", + "quill-image-resize-module": "^3.0.0", "react": "18.3.1", "react-dom": "18.3.1", "react-quill": "^2.0.0"