420 lines
17 KiB
TypeScript
420 lines
17 KiB
TypeScript
import React, { useEffect, useState } from "react";
|
|
import { useForm } from "@inertiajs/react";
|
|
import AuthenticatedLayout from "@/layouts/authenticated-layout";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import { Head } from "@inertiajs/react";
|
|
import { Editor } from "@tinymce/tinymce-react";
|
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
import { AlertCircle } from "lucide-react";
|
|
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
|
|
const slugify = (text: string) => {
|
|
return text
|
|
.toLowerCase()
|
|
.replace(/[^\w\s-]/g, "") // Remove special characters except hyphens
|
|
.replace(/\s+/g, "-") // Replace spaces with hyphens
|
|
.replace(/-+/g, "-") // Replace multiple hyphens with single hyphen
|
|
.trim(); // Remove leading/trailing spaces
|
|
};
|
|
|
|
interface ExistingPost {
|
|
JudulPost: string;
|
|
KategoriId: number;
|
|
}
|
|
|
|
interface Kategori {
|
|
KategoriId: number;
|
|
NamaKategori: string;
|
|
}
|
|
|
|
interface SubKategori {
|
|
SubKategoriId: number;
|
|
KategoriId: number;
|
|
NamaSubKategori: string;
|
|
}
|
|
|
|
interface AddPostProps {
|
|
kategori: Kategori[];
|
|
subkategori: SubKategori[];
|
|
existingPosts: ExistingPost[];
|
|
}
|
|
|
|
export default function AddPost({
|
|
kategori,
|
|
subkategori,
|
|
existingPosts,
|
|
}: AddPostProps) {
|
|
const { toast } = useToast();
|
|
const { data, setData, post, reset } = useForm({
|
|
KategoriId: "",
|
|
SubKategoriId: "",
|
|
JudulPost: "",
|
|
SlugPost: "",
|
|
DescPost: "",
|
|
ImagePost: null as File | null,
|
|
IsPublish: true,
|
|
});
|
|
|
|
const [showAlert, setShowAlert] = useState(false);
|
|
|
|
const [imagePreview, setImagePreview] = useState<string | null>(null);
|
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
|
|
useEffect(() => {
|
|
setData("SlugPost", slugify(data.JudulPost));
|
|
}, [data.JudulPost]);
|
|
|
|
const validateForm = () => {
|
|
const newErrors: Record<string, string> = {};
|
|
|
|
const titleExists = existingPosts.some(
|
|
(post) =>
|
|
post.JudulPost.toLowerCase() === data.JudulPost.toLowerCase() &&
|
|
post.KategoriId === Number(data.KategoriId)
|
|
);
|
|
|
|
if (titleExists) {
|
|
setShowAlert(true);
|
|
newErrors.JudulPost = "Judul sudah ada dalam kategori ini";
|
|
// Auto-hide alert after 5 seconds
|
|
setTimeout(() => setShowAlert(false), 5000);
|
|
}
|
|
|
|
if (!data.KategoriId) {
|
|
newErrors.KategoriId = "Kategori harus dipilih";
|
|
}
|
|
if (!data.SubKategoriId) {
|
|
newErrors.SubKategoriId = "Sub Kategori harus dipilih";
|
|
}
|
|
if (!data.JudulPost || data.JudulPost.trim() === "") {
|
|
newErrors.JudulPost = "Judul Post harus diisi";
|
|
}
|
|
if (!data.DescPost || data.DescPost.trim() === "") {
|
|
newErrors.DescPost = "Deskripsi Post harus diisi";
|
|
}
|
|
if (!data.ImagePost) {
|
|
newErrors.ImagePost = "Gambar harus diunggah";
|
|
}
|
|
|
|
setErrors(newErrors);
|
|
return Object.keys(newErrors).length === 0;
|
|
};
|
|
|
|
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
if (e.target.files && e.target.files[0]) {
|
|
const file = e.target.files[0];
|
|
setData("ImagePost", file);
|
|
|
|
// Create preview URL
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => {
|
|
setImagePreview(reader.result as string);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
};
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
if (!validateForm()) {
|
|
toast({
|
|
title: "Validasi Gagal",
|
|
description: "Silakan periksa kembali form anda",
|
|
variant: "destructive",
|
|
});
|
|
return;
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append("KategoriId", data.KategoriId);
|
|
formData.append("SubKategoriId", data.SubKategoriId);
|
|
formData.append("JudulPost", data.JudulPost);
|
|
formData.append("SlugPost", data.SlugPost);
|
|
formData.append("DescPost", data.DescPost);
|
|
formData.append("IsPublish", data.IsPublish.toString());
|
|
|
|
if (data.ImagePost) {
|
|
formData.append("ImagePost", data.ImagePost);
|
|
}
|
|
|
|
// Debug logs
|
|
console.log("Form Data yang dikirim:");
|
|
console.log("KategoriId:", data.KategoriId);
|
|
console.log("SubKategoriId:", data.SubKategoriId);
|
|
console.log("JudulPost:", data.JudulPost);
|
|
console.log("SlugPost:", data.SlugPost);
|
|
console.log("DescPost:", data.DescPost);
|
|
console.log("IsPublish:", data.IsPublish);
|
|
console.log("ImagePost:", data.ImagePost);
|
|
|
|
// Log FormData entries
|
|
console.log("FormData entries:");
|
|
for (let pair of formData.entries()) {
|
|
console.log(pair[0], pair[1]);
|
|
}
|
|
|
|
post("/admin/post", {
|
|
data: formData,
|
|
forceFormData: true,
|
|
onSuccess: (response) => {
|
|
console.log("Response Success:", response);
|
|
toast({
|
|
title: "Berhasil",
|
|
description: "Post berhasil dibuat",
|
|
variant: "default",
|
|
});
|
|
setImagePreview(null);
|
|
reset();
|
|
},
|
|
onError: (errors) => {
|
|
console.error("Error submitting post:", errors);
|
|
toast({
|
|
title: "Gagal",
|
|
description:
|
|
"Terjadi kesalahan saat membuat Post. Cek console untuk detail error.",
|
|
variant: "destructive",
|
|
});
|
|
},
|
|
});
|
|
};
|
|
|
|
return (
|
|
<AuthenticatedLayout header="Tambah Post">
|
|
<Head title="Tambah Post" />
|
|
<div className="container mx-auto p-4">
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Input
|
|
type="text"
|
|
placeholder="Judul Post"
|
|
value={data.JudulPost}
|
|
onChange={(e) =>
|
|
setData("JudulPost", e.target.value)
|
|
}
|
|
/>
|
|
{errors.JudulPost && (
|
|
<p className="text-red-500 text-sm">
|
|
{errors.JudulPost}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<Input
|
|
type="text"
|
|
placeholder="Slug Post"
|
|
value={data.SlugPost}
|
|
readOnly
|
|
/>
|
|
|
|
<div className="space-y-2">
|
|
<Select
|
|
onValueChange={(value) =>
|
|
setData("KategoriId", value)
|
|
}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Pilih Kategori" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{kategori.map((kat) => (
|
|
<SelectItem
|
|
key={kat.KategoriId}
|
|
value={kat.KategoriId.toString()}
|
|
>
|
|
{kat.NamaKategori}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
{errors.KategoriId && (
|
|
<p className="text-red-500 text-sm">
|
|
{errors.KategoriId}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{data.KategoriId && (
|
|
<div className="space-y-2">
|
|
<Select
|
|
onValueChange={(value) =>
|
|
setData("SubKategoriId", value)
|
|
}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Pilih SubKategori" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{subkategori
|
|
.filter(
|
|
(sub) =>
|
|
sub.KategoriId ===
|
|
Number(data.KategoriId)
|
|
)
|
|
.map((sub) => (
|
|
<SelectItem
|
|
key={sub.SubKategoriId}
|
|
value={sub.SubKategoriId.toString()}
|
|
>
|
|
{sub.NamaSubKategori}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
{errors.SubKategoriId && (
|
|
<p className="text-red-500 text-sm">
|
|
{errors.SubKategoriId}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-2">
|
|
<div className="relative min-h-[300px]">
|
|
<Editor
|
|
apiKey={import.meta.env.VITE_KEY_TINY_MCE}
|
|
onEditorChange={(content) => {
|
|
setData("DescPost", content);
|
|
}}
|
|
init={{
|
|
plugins: [
|
|
// Free plugins only
|
|
"anchor",
|
|
"autolink",
|
|
"charmap",
|
|
"codesample",
|
|
"emoticons",
|
|
"image",
|
|
"link",
|
|
"lists",
|
|
"media",
|
|
"searchreplace",
|
|
"table",
|
|
"visualblocks",
|
|
"wordcount",
|
|
"code",
|
|
"fullscreen",
|
|
"preview",
|
|
],
|
|
toolbar:
|
|
"undo redo | blocks | bold italic underline strikethrough | link image media table | align | bullist numlist | emoticons charmap | fullscreen preview code | removeformat",
|
|
height: 500,
|
|
menubar:
|
|
"file edit view insert format tools table help",
|
|
image_caption: true,
|
|
quickbars_selection_toolbar:
|
|
"bold italic | quicklink h2 h3 blockquote",
|
|
contextmenu: "link image table",
|
|
}}
|
|
initialValue="Isi Artikel"
|
|
/>
|
|
</div>
|
|
{errors.DescPost && (
|
|
<p className="text-red-500 text-sm">
|
|
{errors.DescPost}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-center w-full">
|
|
<label className="flex flex-col items-center justify-center w-full h-64 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 hover:bg-gray-100">
|
|
<div className="flex flex-col items-center justify-center pt-5 pb-6">
|
|
{imagePreview ? (
|
|
<img
|
|
src={imagePreview}
|
|
alt="Preview"
|
|
className="max-h-52 object-contain"
|
|
/>
|
|
) : (
|
|
<>
|
|
<svg
|
|
className="w-8 h-8 mb-4 text-gray-500"
|
|
aria-hidden="true"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 20 16"
|
|
>
|
|
<path
|
|
stroke="currentColor"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth="2"
|
|
d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"
|
|
/>
|
|
</svg>
|
|
<p className="mb-2 text-sm text-gray-500">
|
|
<span className="font-semibold">
|
|
Klik untuk upload
|
|
</span>{" "}
|
|
atau drag and drop
|
|
</p>
|
|
<p className="text-xs text-gray-500">
|
|
PNG, JPG, JPEG atau WEBP (MAX.
|
|
800x400px)
|
|
</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
<Input
|
|
type="file"
|
|
className="hidden"
|
|
accept="image/png, image/jpg, image/jpeg, image/webp"
|
|
onChange={handleImageChange}
|
|
/>
|
|
</label>
|
|
</div>
|
|
{errors.ImagePost && (
|
|
<p className="text-red-500 text-sm">
|
|
{errors.ImagePost}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Select
|
|
value={data.IsPublish.toString()}
|
|
onValueChange={(value) =>
|
|
setData("IsPublish", value === "true")
|
|
}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Pilih Status" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="true">Published</SelectItem>
|
|
<SelectItem value="false">Draft</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="flex justify-start gap-4">
|
|
<Button
|
|
type="button"
|
|
className="bg-gray-600 text-white"
|
|
onClick={() => window.history.back()}
|
|
>
|
|
Kembali
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
className="bg-blue-600 text-white"
|
|
>
|
|
Simpan
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</AuthenticatedLayout>
|
|
);
|
|
}
|