270 lines
8.8 KiB
TypeScript
270 lines
8.8 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useForm } from "react-hook-form";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import * as z from "zod";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from "@/components/ui/form";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardFooter,
|
|
CardHeader,
|
|
CardTitle
|
|
} from "@/components/ui/card";
|
|
import { CheckIcon, Loader2, AlertCircle } from "lucide-react";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { useToast } from "@/components/ui/use-toast";
|
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
|
|
// Form schema
|
|
const formSchema = z.object({
|
|
title: z.string().min(5, { message: "Title must be at least 5 characters" }),
|
|
content: z.string().min(10, { message: "Content must be at least 10 characters" }),
|
|
targetRoles: z.array(z.string()).min(1, { message: "Select at least one target role" }),
|
|
});
|
|
|
|
interface AnnouncementFormProps {
|
|
userRole: string[];
|
|
}
|
|
|
|
export function AnnouncementForm({ userRole }: AnnouncementFormProps) {
|
|
const [selectedRoles, setSelectedRoles] = useState<string[]>([]);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [isSuccess, setIsSuccess] = useState(false);
|
|
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
|
const { toast } = useToast();
|
|
|
|
// Initialize form
|
|
const form = useForm<z.infer<typeof formSchema>>({
|
|
resolver: zodResolver(formSchema),
|
|
defaultValues: {
|
|
title: "",
|
|
content: "",
|
|
targetRoles: [],
|
|
},
|
|
});
|
|
|
|
// Available roles for selection
|
|
const availableRoles = [
|
|
{ id: "all", name: "All Users" },
|
|
{ id: "admin", name: "Administrators" },
|
|
{ id: "entrepreneurship", name: "Entrepreneurship" },
|
|
{ id: "communication", name: "Communication" },
|
|
{ id: "expression", name: "Expression" },
|
|
{ id: "coding", name: "Coding" },
|
|
{ id: "dataintelligence", name: "Data Intelligence" },
|
|
{ id: "mediation", name: "Mediation" },
|
|
];
|
|
|
|
// Handle role selection
|
|
const handleRoleToggle = (roleId: string) => {
|
|
if (roleId === "all") {
|
|
// If "all" is selected, clear other selections
|
|
setSelectedRoles(["all"]);
|
|
form.setValue("targetRoles", ["all"]);
|
|
} else {
|
|
// Remove "all" if it was previously selected
|
|
const newSelection = selectedRoles.filter(id => id !== "all");
|
|
|
|
// Toggle the selected role
|
|
if (newSelection.includes(roleId)) {
|
|
const updatedSelection = newSelection.filter(id => id !== roleId);
|
|
setSelectedRoles(updatedSelection);
|
|
form.setValue("targetRoles", updatedSelection);
|
|
} else {
|
|
const updatedSelection = [...newSelection, roleId];
|
|
setSelectedRoles(updatedSelection);
|
|
form.setValue("targetRoles", updatedSelection);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Form submission
|
|
const onSubmit = async (data: z.infer<typeof formSchema>) => {
|
|
setIsSubmitting(true);
|
|
setErrorMsg(null);
|
|
|
|
try {
|
|
// Send the data to the API
|
|
const response = await fetch('/api/announcements', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(data),
|
|
});
|
|
|
|
// Parse response, even if it's an error
|
|
const result = await response.json();
|
|
|
|
if (!response.ok) {
|
|
let errorMessage = result.error || 'Failed to create announcement';
|
|
if (result.details) {
|
|
errorMessage += `: ${result.details}`;
|
|
}
|
|
console.error("API Error:", result);
|
|
setErrorMsg(errorMessage);
|
|
throw new Error(errorMessage);
|
|
}
|
|
|
|
// Reset form and show success message
|
|
form.reset();
|
|
setSelectedRoles([]);
|
|
setIsSuccess(true);
|
|
|
|
toast({
|
|
title: "Announcement created",
|
|
description: "The announcement has been created successfully.",
|
|
});
|
|
|
|
// Hide success message after a delay
|
|
setTimeout(() => setIsSuccess(false), 3000);
|
|
} catch (error) {
|
|
console.error("Error submitting announcement:", error);
|
|
toast({
|
|
title: "Error",
|
|
description: error instanceof Error ? error.message : "Failed to create the announcement. Please try again.",
|
|
variant: "destructive",
|
|
});
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Card className="bg-white">
|
|
<CardHeader className="bg-white text-gray-800">
|
|
<CardTitle>Create New Announcement</CardTitle>
|
|
<CardDescription>
|
|
Create an announcement to be displayed to specific user roles
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{errorMsg && (
|
|
<Alert variant="destructive" className="mb-6">
|
|
<AlertCircle className="h-4 w-4" />
|
|
<AlertTitle>Error</AlertTitle>
|
|
<AlertDescription>{errorMsg}</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
<Form {...form}>
|
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
<FormField
|
|
control={form.control}
|
|
name="title"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel className="text-gray-700">Title</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
placeholder="Enter announcement title"
|
|
className="bg-white text-gray-800 border-gray-300"
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="content"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel className="text-gray-700">Content</FormLabel>
|
|
<FormControl>
|
|
<Textarea
|
|
placeholder="Enter announcement content"
|
|
rows={5}
|
|
className="bg-white text-gray-800 border-gray-300"
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="targetRoles"
|
|
render={() => (
|
|
<FormItem>
|
|
<FormLabel className="text-gray-700">Target Audience</FormLabel>
|
|
<FormControl>
|
|
<div className="p-3 border border-gray-200 rounded-md bg-gray-50">
|
|
<p className="text-sm text-gray-500 mb-2">Select which roles can see this announcement:</p>
|
|
<div className="flex flex-wrap gap-2">
|
|
{availableRoles.map(role => (
|
|
<Badge
|
|
key={role.id}
|
|
variant={selectedRoles.includes(role.id) ? "default" : "outline"}
|
|
className={`cursor-pointer px-3 py-1 ${
|
|
selectedRoles.includes(role.id)
|
|
? "bg-blue-600 hover:bg-blue-700"
|
|
: "hover:bg-gray-100"
|
|
}`}
|
|
onClick={() => handleRoleToggle(role.id)}
|
|
>
|
|
{role.name}
|
|
{selectedRoles.includes(role.id) && (
|
|
<CheckIcon className="ml-1 h-3 w-3" />
|
|
)}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<div className="flex justify-end">
|
|
<Button
|
|
type="submit"
|
|
disabled={isSubmitting || isSuccess}
|
|
className="px-4"
|
|
>
|
|
{isSubmitting ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
Submitting...
|
|
</>
|
|
) : isSuccess ? (
|
|
<>
|
|
<CheckIcon className="mr-2 h-4 w-4" />
|
|
Announcement Created!
|
|
</>
|
|
) : (
|
|
"Create Announcement"
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</Form>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|