refactor: Perbarui penanganan gambar dan boolean di PostController

main
marszayn 2025-03-10 14:04:45 +07:00
parent 6bb5742623
commit f413f8e4e0
5 changed files with 134 additions and 155 deletions

View File

@ -112,36 +112,42 @@ class PostController extends Controller
} }
public function update(PostRequest $request, Post $post) public function update(PostRequest $request, Post $post)
{ {
try { try {
DB::beginTransaction(); DB::beginTransaction();
$data = $request->validated(); $data = $request->validated();
// Only update image if new one is uploaded // Update gambar hanya jika ada file baru yang diupload
if ($request->hasFile('ImagePost')) { if ($request->hasFile('ImagePost')) {
// Delete old image if exists // Hapus gambar lama jika ada
if ($post->ImagePost && Storage::disk('public')->exists($post->ImagePost)) { if ($post->ImagePost && Storage::disk('public')->exists($post->ImagePost)) {
Storage::disk('public')->delete($post->ImagePost); Storage::disk('public')->delete($post->ImagePost);
}
$data['ImagePost'] = $request->file('ImagePost')->store('images/posts', 'public');
} else {
// Keep existing image if no new one uploaded
unset($data['ImagePost']);
} }
$data['ImagePost'] = $request->file('ImagePost')->store('images/posts', 'public');
$data['IsPublish'] = $request->boolean('IsPublish'); } else {
$post->update($data); // Jika tidak ada file baru, jangan update field ImagePost
unset($data['ImagePost']);
DB::commit();
return redirect()->route('admin.post.index')->with('success', 'Post berhasil diperbarui.');
} catch (\Exception $e) {
DB::rollBack();
Log::error('Error updating Post: ' . $e->getMessage());
return back()->with('error', 'Terjadi kesalahan saat memperbarui post.');
} }
// Pastikan nilai is_publish dikonversi ke boolean
$data['IsPublish'] = filter_var($request->input('IsPublish'), FILTER_VALIDATE_BOOLEAN);
$post->update($data);
DB::commit();
return redirect()->route('admin.post.index')->with('success', 'Post berhasil diperbarui.');
} catch (\Exception $e) {
DB::rollBack();
Log::error('Error updating Post: ' . $e->getMessage());
Log::info('Form data received:', [
'IsPublish' => $request->input('IsPublish'),
'IsPublishType' => gettype($request->input('IsPublish')),
'hasFile' => $request->hasFile('ImagePost')
]);
return back()->with('error', 'Terjadi kesalahan saat memperbarui post.');
} }
}
public function destroy(Post $post) public function destroy(Post $post)
{ {

View File

@ -22,53 +22,14 @@ class PostRequest extends FormRequest
*/ */
public function rules(): array public function rules(): array
{ {
if ($this->isMethod('POST')) { return [
return [ 'KategoriId' => 'required|integer|exists:Kategori,KategoriId',
'KategoriId' => 'required|integer|exists:Kategori,KategoriId', 'SubKategoriId' => 'required|integer|exists:SubKategori,SubKategoriId',
'SubKategoriId' => 'required|integer|exists:SubKategori,SubKategoriId', 'JudulPost' => ['required', 'string', 'max:255'],
'JudulPost' => ['required', 'string', 'max:255'], 'SlugPost' => ['required', 'string', 'max:255'],
'SlugPost' => ['required', 'string', 'max:255'], 'DescPost' => ['required', 'string'],
'DescPost' => ['required', 'string'], 'ImagePost' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'],
'ImagePost' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'], 'IsPublish' => 'boolean',
'IsPublish' => 'boolean', ];
];
}
if ($this->isMethod('PUT') || $this->isMethod('PATCH')) {
return [
'KategoriId' => 'nullable|integer|exists:Kategori,KategoriId',
'SubKategoriId' => 'nullable|integer|exists:SubKategori,SubKategoriId',
'JudulPost' => ['nullable', 'string', 'max:255'],
'SlugPost' => ['nullable', 'string', 'max:255'],
'DescPost' => ['nullable', 'string'],
'ImagePost' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'],
'IsPublish' => 'nullable|boolean',
];
}
return [];
}
// Add this method to handle validation before rules are applied
protected function prepareForValidation()
{
if ($this->isMethod('PUT') || $this->isMethod('PATCH')) {
// Only set these if they're not provided in the request
if (!$this->has('KategoriId')) {
$this->merge(['KategoriId' => $this->route('post')->KategoriId]);
}
if (!$this->has('SubKategoriId')) {
$this->merge(['SubKategoriId' => $this->route('post')->SubKategoriId]);
}
if (!$this->has('JudulPost')) {
$this->merge(['JudulPost' => $this->route('post')->JudulPost]);
}
if (!$this->has('SlugPost')) {
$this->merge(['SlugPost' => $this->route('post')->SlugPost]);
}
if (!$this->has('DescPost')) {
$this->merge(['DescPost' => $this->route('post')->DescPost]);
}
}
} }
} }

View File

@ -282,13 +282,13 @@ export default function AddPost({
<div className="space-y-2"> <div className="space-y-2">
<div className="relative min-h-[300px]"> <div className="relative min-h-[300px]">
<Editor <Editor
apiKey="h471mb13phsh2c8rlp5msca6h0h8y0oy37llvpvzhqjymqq3" apiKey={import.meta.env.VITE_KEY_TINY_MCE}
onEditorChange={(content) => { onEditorChange={(content) => {
setData("DescPost", content); setData("DescPost", content);
}} }}
init={{ init={{
plugins: [ plugins: [
// Core editing features // Free plugins only
"anchor", "anchor",
"autolink", "autolink",
"charmap", "charmap",
@ -302,60 +302,21 @@ export default function AddPost({
"table", "table",
"visualblocks", "visualblocks",
"wordcount", "wordcount",
"checklist", "code",
"mediaembed", "fullscreen",
"casechange", "preview",
"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: 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", "undo redo | blocks | bold italic underline strikethrough | link image media table | align | bullist numlist | emoticons charmap | fullscreen preview code | removeformat",
tinycomments_mode: "embedded", height: 500,
tinycomments_author: "Author name", menubar:
mergetags_list: [ "file edit view insert format tools table help",
{ image_caption: true,
value: "First.Name", quickbars_selection_toolbar:
title: "First Name", "bold italic | quicklink h2 h3 blockquote",
}, contextmenu: "link image table",
{ 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!" initialValue="Isi Artikel"
/> />
</div> </div>
{errors.DescPost && ( {errors.DescPost && (
@ -436,10 +397,21 @@ export default function AddPost({
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className="flex justify-start gap-4">
<Button type="submit" className="bg-blue-600 text-white"> <Button
Simpan type="button"
</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> </form>
</div> </div>
</AuthenticatedLayout> </AuthenticatedLayout>

View File

@ -26,7 +26,7 @@ interface SubKategori {
NamaSubKategori: string; NamaSubKategori: string;
} }
interface Post { interface Posting {
PostId: number; PostId: number;
KategoriId: number; KategoriId: number;
SubKategoriId: number; SubKategoriId: number;
@ -38,7 +38,7 @@ interface Post {
} }
interface EditPostProps { interface EditPostProps {
post: Post; posting: Posting;
kategori: Kategori[]; kategori: Kategori[];
subkategori: SubKategori[]; subkategori: SubKategori[];
} }
@ -56,28 +56,29 @@ interface PostFormData {
const slugify = (text: string) => text.toLowerCase().replace(/\s+/g, "-"); const slugify = (text: string) => text.toLowerCase().replace(/\s+/g, "-");
export default function EditPost({ export default function EditPost({
post, posting,
kategori, kategori,
subkategori, subkategori,
}: EditPostProps) { }: EditPostProps) {
const { toast } = useToast(); const { toast } = useToast();
const [imagePreview, setImagePreview] = useState<string | null>(null); const [imagePreview, setImagePreview] = useState<string | null>(null);
const { data, setData, put, processing, errors } = useForm<PostFormData>({ const { data, setData, post, processing, errors } = useForm<PostFormData>({
KategoriId: post.KategoriId.toString(), KategoriId: posting.KategoriId.toString(),
SubKategoriId: post.SubKategoriId.toString(), SubKategoriId: posting.SubKategoriId.toString(),
JudulPost: post.JudulPost, JudulPost: posting.JudulPost,
SlugPost: post.SlugPost, SlugPost: posting.SlugPost,
DescPost: post.DescPost, DescPost: posting.DescPost,
ImagePost: null, ImagePost: null,
IsPublish: post.IsPublish, IsPublish: posting.IsPublish,
}); });
useEffect(() => { useEffect(() => {
if (post.ImagePost) { if (posting.ImagePost) {
setImagePreview(`/storage/${post.ImagePost}`); const path = `${posting.ImagePost}`;
setImagePreview(path);
} }
}, [post.ImagePost]); }, [posting.ImagePost]);
useEffect(() => { useEffect(() => {
if (data.JudulPost && data.JudulPost.trim() !== "") { if (data.JudulPost && data.JudulPost.trim() !== "") {
@ -107,13 +108,13 @@ export default function EditPost({
formData.append("JudulPost", data.JudulPost); formData.append("JudulPost", data.JudulPost);
formData.append("SlugPost", data.SlugPost); formData.append("SlugPost", data.SlugPost);
formData.append("DescPost", data.DescPost); formData.append("DescPost", data.DescPost);
// Send the boolean value directly
formData.append("IsPublish", data.IsPublish.toString()); formData.append("IsPublish", data.IsPublish.toString());
if (data.ImagePost instanceof File) { if (data.ImagePost instanceof File) {
formData.append("ImagePost", data.ImagePost); formData.append("ImagePost", data.ImagePost);
} }
put(`/admin/post/${post.PostId}`, { post(`/admin/post/${posting.PostId}`, {
data: formData, data: formData,
headers: { "Content-Type": "multipart/form-data" }, headers: { "Content-Type": "multipart/form-data" },
onSuccess: () => { onSuccess: () => {
@ -234,24 +235,60 @@ export default function EditPost({
<div> <div>
<Editor <Editor
apiKey="h471mb13phsh2c8rlp5msca6h0h8y0oy37llvpvzhqjymqq3" apiKey={import.meta.env.VITE_KEY_TINY_MCE}
value={data.DescPost} value={data.DescPost}
onEditorChange={(content) => onEditorChange={(content) =>
setData("DescPost", content) setData("DescPost", content)
} }
init={{ init={{
height: 300,
menubar: false,
plugins: [ plugins: [
"advlist autolink lists link image charmap print preview anchor", // Free plugins only
"searchreplace visualblocks code fullscreen", "anchor",
"insertdatetime media table paste code help wordcount", "autolink",
"charmap",
"codesample",
"emoticons",
"image",
"link",
"lists",
"media",
"searchreplace",
"table",
"visualblocks",
"wordcount",
"code",
"fullscreen",
"preview",
], ],
toolbar: toolbar:
"undo redo | formatselect | bold italic backcolor | alignleft aligncenter alignright alignjustify | " + "undo redo | blocks | bold italic underline strikethrough | link image media table | align | bullist numlist | emoticons charmap | fullscreen preview code | removeformat",
"bullist numlist outdent indent | removeformat | help", height: 300,
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 className="mt-2 p-2 bg-gray-50 text-xs text-gray-600 rounded">
<p className="font-medium">SEO Tips:</p>
<ul className="list-disc pl-4 mt-1">
<li>
Gunakan heading tags (H1, H2, H3) dengan
struktur yang tepat
</li>
<li>Tambahkan alt text pada gambar</li>
<li>
Gunakan link dengan atribut rel yang sesuai
</li>
<li>
Sertakan kata kunci utama dalam paragraf
awal
</li>
</ul>
</div> */}
{errors.DescPost && ( {errors.DescPost && (
<p className="text-red-500 text-sm"> <p className="text-red-500 text-sm">
{errors.DescPost} {errors.DescPost}
@ -334,10 +371,10 @@ export default function EditPost({
<div> <div>
<Select <Select
value={data.IsPublish.toString()}
onValueChange={(value) => onValueChange={(value) =>
setData("IsPublish", value === "true") setData("IsPublish", value === "true")
} }
value={data.IsPublish.toString()}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Pilih Status Publikasi" /> <SelectValue placeholder="Pilih Status Publikasi" />

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Link, useForm } from "@inertiajs/react"; import { Link, useForm, usePage } from "@inertiajs/react";
import { PageProps } from "@/types"; import { PageProps } from "@/types";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@ -24,6 +24,7 @@ import {
import AuthenticatedLayout from "@/layouts/authenticated-layout"; import AuthenticatedLayout from "@/layouts/authenticated-layout";
import { Head } from "@inertiajs/react"; import { Head } from "@inertiajs/react";
import { Toaster } from "@/components/ui/toaster"; import { Toaster } from "@/components/ui/toaster";
import hasAnyPermission from "@/utils/hasAnyPermission";
interface SubKategori { interface SubKategori {
SubKategoriId: number; SubKategoriId: number;
@ -49,6 +50,8 @@ const ITEMS_PER_PAGE = 5;
export default function PostIndex({ export default function PostIndex({
posts = [], posts = [],
}: PageProps<{ posts: Posting[] }>) { }: PageProps<{ posts: Posting[] }>) {
const { auth } = usePage().props;
const userPermissions = auth?.user?.permissions ?? [];
const { toast } = useToast(); const { toast } = useToast();
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);