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

448 lines
19 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="h471mb13phsh2c8rlp5msca6h0h8y0oy37llvpvzhqjymqq3"
onEditorChange={(content) => {
setData("DescPost", content);
}}
init={{
plugins: [
// Core editing features
"anchor",
"autolink",
"charmap",
"codesample",
"emoticons",
"image",
"link",
"lists",
"media",
"searchreplace",
"table",
"visualblocks",
"wordcount",
"checklist",
"mediaembed",
"casechange",
"export",
"formatpainter",
"pageembed",
"a11ychecker",
"tinymcespellchecker",
"permanentpen",
"powerpaste",
"advtable",
"advcode",
"editimage",
"advtemplate",
"ai",
"mentions",
"tinycomments",
"tableofcontents",
"footnotes",
"mergetags",
"autocorrect",
"typography",
"inlinecss",
"markdown",
"importword",
"exportword",
"exportpdf",
],
toolbar:
"undo redo | blocks fontfamily fontsize | bold italic underline strikethrough | link image media table mergetags | addcomment showcomments | spellcheckdialog a11ycheck typography | align lineheight | checklist numlist bullist indent outdent | emoticons charmap | removeformat",
tinycomments_mode: "embedded",
tinycomments_author: "Author name",
mergetags_list: [
{
value: "First.Name",
title: "First Name",
},
{ value: "Email", title: "Email" },
],
ai_request: (
_request: any,
respondWith: {
string: (
callback: () => Promise<string>
) => void;
}
) =>
respondWith.string(() =>
Promise.reject(
"See docs to implement AI Assistant"
)
),
}}
initialValue="Welcome to TinyMCE!"
/>
</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>
<Button type="submit" className="bg-blue-600 text-white">
Simpan
</Button>
</form>
</div>
</AuthenticatedLayout>
);
}