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,182 @@
|
||||
// modules/admin/pages/notifications/AddNotificationBroadcast.jsx
|
||||
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { House } from "lucide-react";
|
||||
|
||||
import { useNotificationBroadcasts } from "@/contexts/AdminNotificationBroadcastContext";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import AppBreadcrumb from "@/components/generic/Breadcrumb/AppBreadcrumb";
|
||||
import { BroadcastTargetPicker } from "@/components/generic/BroadcastTargetPicker";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
|
||||
import { TARGET_TYPE_OPTIONS, TARGET_TYPE_MAP } from "@/data/notificationBroadcast.data";
|
||||
|
||||
// ─── Schema ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const schema = z.object({
|
||||
title: z.string().min(1, "Title is required."),
|
||||
message: z.string().min(1, "Message is required."),
|
||||
target_type: z.enum(["admin", "user", "both", "task_list", "course", "tier_plan"], { required_error: "Target is required." }),
|
||||
target_id: z.string().nullable().optional(),
|
||||
}).superRefine((data, ctx) => {
|
||||
if (TARGET_TYPE_MAP[data.target_type]?.needsTarget && !data.target_id) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Please select a specific target.",
|
||||
path: ["target_id"],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
function FieldError({ message }) {
|
||||
if (!message) return null;
|
||||
return <p className="text-xs text-destructive mt-1">{message}</p>;
|
||||
}
|
||||
|
||||
function SectionCard({ title, description, children }) {
|
||||
return (
|
||||
<div className="rounded-lg border bg-card p-6 space-y-5">
|
||||
{(title || description) && (
|
||||
<div className="space-y-0.5 pb-1 border-b">
|
||||
{title && <h2 className="text-sm font-semibold">{title}</h2>}
|
||||
{description && <p className="text-xs text-muted-foreground">{description}</p>}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Page ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function AddNotificationBroadcast() {
|
||||
const navigate = useNavigate();
|
||||
const { createBroadcast, loading } = useNotificationBroadcasts();
|
||||
const { user } = useAuth();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
title: "",
|
||||
message: "",
|
||||
target_type: undefined,
|
||||
target_id: null,
|
||||
},
|
||||
});
|
||||
|
||||
const targetType = watch("target_type");
|
||||
const targetId = watch("target_id");
|
||||
const needsTarget = TARGET_TYPE_MAP[targetType]?.needsTarget;
|
||||
|
||||
const breadcrumbItems = [
|
||||
{ label: "Home", icon: <House className="size-4" />, to: "/admin" },
|
||||
{ label: "Notifications", to: "/admin/notifications" },
|
||||
{ label: "New" },
|
||||
];
|
||||
|
||||
const onSubmit = async (values) => {
|
||||
const payload = {
|
||||
...values,
|
||||
target_id: TARGET_TYPE_MAP[values.target_type]?.needsTarget ? values.target_id : null,
|
||||
createdBy: user?.user_id ?? null,
|
||||
};
|
||||
|
||||
const res = await createBroadcast(payload);
|
||||
if (res) navigate("/admin/notifications");
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="bg-muted h-full">
|
||||
<div className="lg:container lg:mx-auto lg:px-0 flex flex-col items-center px-4">
|
||||
<div className="flex flex-col gap-2 my-6 w-full">
|
||||
<AppBreadcrumb items={breadcrumbItems} />
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-2xl pb-10">
|
||||
<h1 className="text-2xl font-semibold tracking-tight mb-1">New notification</h1>
|
||||
<p className="text-sm text-muted-foreground mb-6">Compose an announcement. It's saved as a draft until you send it.</p>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-5">
|
||||
|
||||
<SectionCard title="Content" description="What admins and/or users will see.">
|
||||
<div>
|
||||
<Label className="mb-1.5 block">Title</Label>
|
||||
<Input placeholder="e.g. Scheduled maintenance tonight" {...register("title")} />
|
||||
<FieldError message={errors.title?.message} />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="mb-1.5 block">Message</Label>
|
||||
<Textarea rows={4} placeholder="Full announcement text" {...register("message")} />
|
||||
<FieldError message={errors.message?.message} />
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard title="Target" description="Who receives this notification when it's sent.">
|
||||
<div>
|
||||
<Select
|
||||
value={targetType}
|
||||
onValueChange={(v) => {
|
||||
setValue("target_type", v, { shouldValidate: true });
|
||||
setValue("target_id", null);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a target" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{TARGET_TYPE_OPTIONS.map((t) => (
|
||||
<SelectItem key={t.value} value={t.value}>{t.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FieldError message={errors.target_type?.message} />
|
||||
{targetType && (
|
||||
<p className="text-xs text-muted-foreground mt-1.5">
|
||||
{TARGET_TYPE_MAP[targetType]?.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{needsTarget && (
|
||||
<div>
|
||||
<BroadcastTargetPicker
|
||||
targetType={targetType}
|
||||
value={targetId}
|
||||
onChange={(id) => setValue("target_id", id, { shouldValidate: true })}
|
||||
/>
|
||||
<FieldError message={errors.target_id?.message} />
|
||||
</div>
|
||||
)}
|
||||
</SectionCard>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button type="button" variant="outline" onClick={() => navigate(-1)} disabled={loading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading && <Spinner className="h-4 w-4 mr-2" />}
|
||||
Save draft
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user