mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 19:58:11 +00:00
420 lines
15 KiB
TypeScript
420 lines
15 KiB
TypeScript
import { useForm } from "react-hook-form";
|
|
import { Button } from "@/shared/common/ui/button";
|
|
import { Label } from "@/shared/common/ui/label";
|
|
import { Textarea } from "@/shared/common/ui/textarea";
|
|
import { ImageIcon, Loader2 } from "lucide-react";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import { FormFeild } from "../components/FormFeild";
|
|
import { CreateNewsPayload, NewsItem } from "../types/newsTypes";
|
|
import { useNewsCategory } from "../hooks/useNewsCategory";
|
|
import { useNavigate, useParams } from "react-router-dom";
|
|
import { useNews } from "../hooks/useNews";
|
|
import { toast } from "sonner";
|
|
import { ContactUsParams } from "../hooks/useContactUs";
|
|
|
|
interface FormData {
|
|
titleEn: string;
|
|
titleAm: string;
|
|
subTitleAm: string;
|
|
subTitleEn: string;
|
|
bodyAm: string;
|
|
bodyEn: string;
|
|
newsCategoryId: string;
|
|
image: FileList;
|
|
isActive: boolean;
|
|
isFeatured: boolean;
|
|
}
|
|
|
|
export const EditNews = () => {
|
|
const { id } = useParams();
|
|
const { singleItem: item, updateItem } = useNews(undefined, id);
|
|
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
|
const navigate = useNavigate();
|
|
const defaultValues = useMemo(
|
|
() => ({
|
|
titleEn: item?.title?.en || "",
|
|
titleAm: item?.title?.am || "",
|
|
subTitleAm: item?.subTitle?.am || "",
|
|
subTitleEn: item?.subTitle?.en || "",
|
|
bodyAm: item?.body?.am || "",
|
|
bodyEn: item?.body?.en || "",
|
|
newsCategoryId: item?.newsCategoryId || "",
|
|
isActive: item?.isActive || false,
|
|
isFeatured: item?.isFeatured || false,
|
|
image: undefined,
|
|
}),
|
|
[item] // 👈 recompute only when item changes
|
|
);
|
|
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
watch,
|
|
setValue,
|
|
reset,
|
|
formState: { errors },
|
|
} = useForm<FormData>({
|
|
defaultValues,
|
|
});
|
|
useEffect(() => {
|
|
if (item) {
|
|
reset(defaultValues);
|
|
}
|
|
}, [item, defaultValues, reset]);
|
|
const imageFile = watch("image")?.[0];
|
|
const [imagePreview, setImagePreview] = useState<string>(
|
|
item?.presigned || ""
|
|
);
|
|
|
|
// Fetch news categories for dropdown
|
|
const { items: categories } = useNewsCategory();
|
|
|
|
useEffect(() => {
|
|
if (imageFile) {
|
|
const url = URL.createObjectURL(imageFile);
|
|
setImagePreview(url);
|
|
return () => URL.revokeObjectURL(url);
|
|
}
|
|
}, [imageFile]);
|
|
|
|
const ImageUpload = ({
|
|
label,
|
|
preview,
|
|
existingName,
|
|
register,
|
|
error,
|
|
onView,
|
|
id,
|
|
isRequired = false,
|
|
}: {
|
|
label: string;
|
|
preview: string | null;
|
|
existingName?: string;
|
|
register: any;
|
|
error?: string;
|
|
onView: () => void;
|
|
id: string;
|
|
isRequired?: boolean;
|
|
}) => (
|
|
<div className="space-y-2">
|
|
<Label className="text-gray-700 block mb-2">
|
|
{label} {isRequired && "*"}
|
|
</Label>
|
|
|
|
{preview && (
|
|
<div className="mb-3 flex items-center justify-between bg-primary-50 p-3 rounded-lg">
|
|
<div className="flex items-center">
|
|
<ImageIcon className="h-5 w-5 text-primary-500 mr-2" />
|
|
<span className="text-sm font-medium truncate max-w-[200px]">
|
|
{existingName || "Selected Image"}
|
|
</span>
|
|
</div>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={onView}
|
|
className="text-primary-600 hover:text-primary-800"
|
|
>
|
|
Preview
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<FormFeild
|
|
id={id}
|
|
type="file"
|
|
accept="image/*"
|
|
{...register}
|
|
error={error}
|
|
/>
|
|
</div>
|
|
|
|
{error && <p className="text-xs text-red-500 mt-1">{error}</p>}
|
|
</div>
|
|
);
|
|
const handleEditSubmit = async (formData: FormData) => {
|
|
if (!item) return;
|
|
setIsSubmitting(true);
|
|
try {
|
|
const imageFile = formData.image?.[0];
|
|
|
|
const payload: CreateNewsPayload = {
|
|
newsCategoryId: formData.newsCategoryId,
|
|
title: {
|
|
am: formData.titleAm || "",
|
|
en: formData.titleEn || "",
|
|
},
|
|
subTitle: {
|
|
am: formData.subTitleAm || "",
|
|
en: formData.subTitleEn || "",
|
|
},
|
|
body: {
|
|
am: formData.bodyAm || "",
|
|
en: formData.bodyEn || "",
|
|
},
|
|
// Only include fileInfo if there's a new image
|
|
...(imageFile && {
|
|
fileInfo: {
|
|
fileName: imageFile.name,
|
|
contentType: imageFile.type,
|
|
size: imageFile.size,
|
|
originalname: imageFile.name,
|
|
},
|
|
}),
|
|
isActive: formData.isActive,
|
|
isFeatured: formData.isFeatured,
|
|
};
|
|
|
|
await updateItem({
|
|
id: item.id,
|
|
data: payload,
|
|
...(imageFile && { imageFile }),
|
|
});
|
|
toast.success("News updated successfully");
|
|
navigate(-1);
|
|
} catch (error) {
|
|
toast.error("Failed to update news");
|
|
console.error("Update error:", error);
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
return (
|
|
<div className="max-h-[80vh] overflow-y-auto p-6 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm">
|
|
<form onSubmit={handleSubmit(handleEditSubmit)} className="space-y-8">
|
|
{/* Category Selection */}
|
|
<div className="bg-gray-50 dark:bg-gray-700 p-5 rounded-lg border border-gray-100 dark:border-gray-600">
|
|
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center">
|
|
<div className="h-1 w-4 bg-primary-500 mr-2 rounded-full"></div>
|
|
Category
|
|
</h3>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="newsCategoryId" className="text-gray-700 dark:text-gray-300">
|
|
News Category*
|
|
</Label>
|
|
<select
|
|
id="newsCategoryId"
|
|
{...register("newsCategoryId", {
|
|
required: "Category is required",
|
|
})}
|
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500 dark:bg-gray-600 dark:text-gray-200"
|
|
>
|
|
<option value="">Select a category</option>
|
|
{categories?.map((category) => (
|
|
<option key={category.id} value={category.id}>
|
|
{category.newsCategoryTitle?.en || "Unnamed Category"}
|
|
</option>
|
|
))}
|
|
</select>
|
|
{errors.newsCategoryId && (
|
|
<p className="text-xs text-red-500 mt-1">
|
|
{errors.newsCategoryId.message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Title Section */}
|
|
<div className="bg-gray-50 dark:bg-gray-700 p-5 rounded-lg border border-gray-100 dark:border-gray-600">
|
|
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-100 mb-4 flex items-center">
|
|
<div className="h-1 w-4 bg-primary-500 mr-2 rounded-full"></div>
|
|
Title
|
|
</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="titleAm" className="text-gray-700 dark:text-gray-200">
|
|
Title (Amharic)*
|
|
</Label>
|
|
<FormFeild
|
|
id="titleAm"
|
|
{...register("titleAm", { required: "Required" })}
|
|
error={errors.titleAm?.message}
|
|
placeholder="News title in Amharic"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="titleEn" className="text-gray-700 dark:text-gray-200">
|
|
Title (English)*
|
|
</Label>
|
|
<FormFeild
|
|
id="titleEn"
|
|
{...register("titleEn", { required: "Required" })}
|
|
error={errors.titleEn?.message}
|
|
placeholder="News title in English"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Subtitle Section */}
|
|
<div className="bg-gray-50 dark:bg-gray-700 p-5 rounded-lg border border-gray-100 dark:border-gray-600">
|
|
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-100 mb-4 flex items-center">
|
|
<div className="h-1 w-4 bg-primary-500 mr-2 rounded-full"></div>
|
|
Subtitle
|
|
</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="subTitleAm" className="text-gray-700 dark:text-gray-200">
|
|
Subtitle (Amharic)*
|
|
</Label>
|
|
<FormFeild
|
|
id="subTitleAm"
|
|
{...register("subTitleAm", { required: "Required" })}
|
|
error={errors.subTitleAm?.message}
|
|
placeholder="News subtitle in Amharic"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="subTitleEn" className="text-gray-700 dark:text-gray-200">
|
|
Subtitle (English)*
|
|
</Label>
|
|
<FormFeild
|
|
id="subTitleEn"
|
|
{...register("subTitleEn", { required: "Required" })}
|
|
error={errors.subTitleEn?.message}
|
|
placeholder="News subtitle in English"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Body Section */}
|
|
<div className="bg-gray-50 dark:bg-gray-700 p-5 rounded-lg border border-gray-100 dark:border-gray-600">
|
|
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-100 mb-4 flex items-center">
|
|
<div className="h-1 w-4 bg-purple-500 mr-2 rounded-full"></div>
|
|
Content
|
|
</h3>
|
|
<div className="space-y-6">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="bodyAm" className="text-gray-700 dark:text-gray-200">
|
|
Content (Amharic)*
|
|
</Label>
|
|
<Textarea
|
|
id="bodyAm"
|
|
{...register("bodyAm", { required: "Required" })}
|
|
rows={6}
|
|
placeholder="News content in Amharic"
|
|
className="min-h-[150px] dark:bg-gray-600 dark:text-gray-200"
|
|
/>
|
|
{errors.bodyAm && (
|
|
<p className="text-xs text-red-500 mt-1">
|
|
{errors.bodyAm.message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="bodyEn" className="text-gray-700 dark:text-gray-200">
|
|
Content (English)*
|
|
</Label>
|
|
<Textarea
|
|
id="bodyEn"
|
|
{...register("bodyEn", { required: "Required" })}
|
|
rows={6}
|
|
placeholder="News content in English"
|
|
className="min-h-[150px] dark:bg-gray-600 dark:text-gray-200"
|
|
/>
|
|
{errors.bodyEn && (
|
|
<p className="text-xs text-red-500 mt-1">
|
|
{errors.bodyEn.message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Status Section */}
|
|
<div className="bg-gray-50 dark:bg-gray-700 p-5 rounded-lg border border-gray-100 dark:border-gray-600">
|
|
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-100 mb-4 flex items-center">
|
|
<div className="h-1 w-4 bg-orange-500 mr-2 rounded-full"></div>
|
|
Settings
|
|
</h3>
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<Label htmlFor="isActive" className="text-gray-700 dark:text-gray-200 font-medium">
|
|
Is Active
|
|
</Label>
|
|
<button
|
|
type="button"
|
|
onClick={() => setValue("isActive", !watch("isActive"))}
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
|
watch("isActive") ? "bg-primary-500" : "bg-gray-300"
|
|
}`}
|
|
>
|
|
<span
|
|
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
watch("isActive") ? "translate-x-6" : "translate-x-1"
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<Label htmlFor="isFeatured" className="text-gray-700 font-medium">
|
|
Is Featured
|
|
</Label>
|
|
<button
|
|
type="button"
|
|
onClick={() => setValue("isFeatured", !watch("isFeatured"))}
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
|
watch("isFeatured") ? "bg-yellow-500" : "bg-gray-300"
|
|
}`}
|
|
>
|
|
<span
|
|
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
watch("isFeatured") ? "translate-x-6" : "translate-x-1"
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Image Upload */}
|
|
<div className="bg-gray-50 dark:bg-gray-700 p-5 rounded-lg border border-gray-100 dark:border-gray-600">
|
|
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-100 mb-4 flex items-center">
|
|
<div className="h-1 w-4 bg-yellow-500 mr-2 rounded-full"></div>
|
|
Image
|
|
</h3>
|
|
<ImageUpload
|
|
label="News Image"
|
|
preview={imagePreview}
|
|
existingName={item?.fileInfo?.originalname}
|
|
register={register("image")}
|
|
error={errors.image?.message}
|
|
onView={() => window.open(imagePreview, "_blank")}
|
|
id="image"
|
|
isRequired={false}
|
|
/>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="flex justify-end gap-3 pt-6 border-t border-gray-200 dark:border-gray-700">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => navigate(-1)}
|
|
type="button"
|
|
disabled={isSubmitting}
|
|
className="px-6 py-2 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700"
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
disabled={isSubmitting}
|
|
className="px-6 py-2 bg-purple-600 hover:bg-purple-700 text-white"
|
|
>
|
|
{isSubmitting ? (
|
|
<span className="flex items-center gap-2">
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
Updating...
|
|
</span>
|
|
) : (
|
|
"Update News"
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
};
|