skl/resources/js/pages/admin/post/add_post.tsx

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>
);
}