mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
add: more commits
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { House, Pencil, Bell, Lock, Send, Clock3 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import AppBreadcrumb from "@/components/generic/Breadcrumb/AppBreadcrumb";
|
||||
import { PageMeta } from "@/contexts/MetadataContext";
|
||||
import {
|
||||
AdminNotificationTemplateProvider,
|
||||
useAdminNotificationTemplates,
|
||||
} from "@/contexts/AdminNotificationTemplateContext";
|
||||
import { NOTIFICATION_TEMPLATE_TYPES, getNotificationTemplateType } from "@/data/notificationTemplateTypes.data";
|
||||
import { STATUS_META, hasPendingChanges } from "@/data/notificationTemplateStatus.data";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function TemplateCard({ item, onEdit }) {
|
||||
const typeMeta = getNotificationTemplateType(item.notify_type);
|
||||
const TypeIcon = typeMeta?.icon ?? Bell;
|
||||
const status = STATUS_META[item.status] ?? STATUS_META.draft;
|
||||
const pending = hasPendingChanges(item);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border bg-card p-5 flex flex-col gap-4 h-full">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="w-10 h-10 rounded-lg border bg-muted flex items-center justify-center shrink-0">
|
||||
<TypeIcon className="h-4.5 w-4.5 text-muted-foreground" />
|
||||
</div>
|
||||
<Button type="button" variant="ghost" size="icon" onClick={() => onEdit(item)}>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 flex-wrap mb-1">
|
||||
<p className="text-sm font-semibold truncate">{item.label}</p>
|
||||
{item.is_system && (
|
||||
<Badge variant="secondary" className="gap-1 shrink-0">
|
||||
<Lock className="h-2.5 w-2.5" /> System
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<code className="text-[10px] bg-muted px-1.5 py-0.5 rounded text-muted-foreground">{item.type}</code>
|
||||
<p className="text-xs text-muted-foreground mt-2 line-clamp-2">
|
||||
<span className="text-foreground">{item.title || item.draft_title || "No title yet"}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
{typeMeta && (
|
||||
<Badge variant="outline" className="gap-1 text-[11px]">
|
||||
<typeMeta.icon className="h-3 w-3" /> {typeMeta.label}
|
||||
</Badge>
|
||||
)}
|
||||
<Badge variant="outline" className={cn("gap-1 text-[11px]", status.badgeClass)}>
|
||||
<Send className="h-3 w-3" /> {status.label}
|
||||
</Badge>
|
||||
{pending && (
|
||||
<Badge variant="outline" className="gap-1 text-[11px] bg-amber-100 text-amber-700 border-amber-400 dark:bg-amber-900/40 dark:text-amber-400 dark:border-amber-700">
|
||||
<Clock3 className="h-3 w-3" /> Pending changes
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NotificationTemplatesInner() {
|
||||
const navigate = useNavigate();
|
||||
const { templates, loading, fetchTemplates } = useAdminNotificationTemplates();
|
||||
const [activeType, setActiveType] = useState("all");
|
||||
|
||||
useEffect(() => { fetchTemplates(); }, []);
|
||||
|
||||
const filtered = useMemo(
|
||||
() => activeType === "all" ? templates : templates.filter((t) => t.notify_type === activeType),
|
||||
[templates, activeType]
|
||||
);
|
||||
|
||||
const typesInUse = useMemo(
|
||||
() => NOTIFICATION_TEMPLATE_TYPES.filter((t) => templates.some((tpl) => tpl.notify_type === t.value)),
|
||||
[templates]
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="bg-muted/60 min-h-full">
|
||||
<PageMeta title="Notification Templates - STARR" />
|
||||
<div className="lg:container lg:mx-auto lg:px-0 flex flex-col items-start px-4 py-10">
|
||||
<div className="w-full max-w-6xl mx-auto">
|
||||
|
||||
<div className="flex flex-col gap-2 mb-6">
|
||||
<AppBreadcrumb items={[
|
||||
{ label: "Home", icon: <House className="size-4" />, to: "/admin" },
|
||||
{ label: "Notifications", to: "/admin/notifications" },
|
||||
{ label: "Templates" },
|
||||
]} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold">Notification Templates</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">
|
||||
Title and message wording for every automated notification STARR sends.
|
||||
</p>
|
||||
<div className="flex items-center gap-3 mt-2 text-xs text-muted-foreground">
|
||||
<span className="flex items-center gap-1">
|
||||
<Send className="h-3 w-3 text-emerald-600" />
|
||||
{templates.filter((t) => t.status === "sent").length} sent
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Pencil className="h-3 w-3" />
|
||||
{templates.filter((t) => t.status === "draft").length} draft
|
||||
</span>
|
||||
{templates.some(hasPendingChanges) && (
|
||||
<span className="flex items-center gap-1 text-amber-600">
|
||||
<Clock3 className="h-3 w-3" />
|
||||
{templates.filter(hasPendingChanges).length} with pending changes
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border bg-card p-4 flex items-start gap-3 mb-5">
|
||||
<Lock className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
|
||||
<div className="text-xs text-muted-foreground space-y-1">
|
||||
<p>
|
||||
Every template here is <strong>system</strong>-triggered — code fires it by referencing its
|
||||
exact type, so no template can be added or removed from this screen. Only the title and
|
||||
message wording is editable.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Draft vs. Sent:</strong> a <strong>Sent</strong> template is the version actually
|
||||
used for real notifications right now. Editing a Sent template doesn't change what goes out
|
||||
immediately — it's held as a pending change until you press <strong>Publish</strong> again.
|
||||
</p>
|
||||
<p>
|
||||
Only plain text is supported — no HTML, no conditional logic, just straight{" "}
|
||||
<code className="bg-muted px-1 rounded">{"{{placeholder}}"}</code> tokens that get swapped
|
||||
for real values when the notification fires.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2 mb-5">
|
||||
<Button
|
||||
type="button" size="sm" variant={activeType === "all" ? "secondary" : "outline"}
|
||||
onClick={() => setActiveType("all")}
|
||||
>
|
||||
All ({templates.length})
|
||||
</Button>
|
||||
{typesInUse.map((t) => {
|
||||
const Icon = t.icon;
|
||||
const count = templates.filter((tpl) => tpl.notify_type === t.value).length;
|
||||
return (
|
||||
<Button
|
||||
key={t.value}
|
||||
type="button" size="sm"
|
||||
variant={activeType === t.value ? "secondary" : "outline"}
|
||||
onClick={() => setActiveType(t.value)}
|
||||
className="gap-1.5"
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" /> {t.label} ({count})
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Separator className="mb-5" />
|
||||
|
||||
{loading && !templates.length ? (
|
||||
<div className="flex justify-center py-12"><Spinner className="h-5 w-5" /></div>
|
||||
) : !filtered.length ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-12">No notification templates found.</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{filtered.map((item) => (
|
||||
<TemplateCard
|
||||
key={item.notification_template_id}
|
||||
item={item}
|
||||
onEdit={(t) => navigate(`/admin/notification-templates/${t.notification_template_id}/edit`)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default function NotificationTemplates() {
|
||||
return (
|
||||
<AdminNotificationTemplateProvider>
|
||||
<NotificationTemplatesInner />
|
||||
</AdminNotificationTemplateProvider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user