mediahub-fe/components/form/content/spit-convert-form.tsx

1213 lines
38 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, { ChangeEvent, useEffect, useRef, useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { useParams, useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import dynamic from "next/dynamic";
import Cookies from "js-cookie";
// UI Components
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Separator } from "@/components/ui/separator";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
// Icons
import {
AlertCircle,
FileText,
Image,
Loader2,
Save,
Trash2,
Edit3,
Globe,
Users,
Tag,
Eye,
Settings,
CheckCircle,
XCircle,
} from "lucide-react";
// Swiper
import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css";
import "swiper/css/free-mode";
import "swiper/css/navigation";
import "swiper/css/pagination";
import "swiper/css/thumbs";
import { FreeMode, Navigation, Pagination, Thumbs } from "swiper/modules";
// Services
import {
convertSPIT,
deleteSPIT,
detailSPIT,
getTagsBySubCategoryId,
listEnableCategory,
} from "@/service/content/content";
import { generateDataRewrite, getDetailArticle } from "@/service/content/ai";
// Utils
import { getCookiesDecrypt } from "@/lib/utils";
import { error } from "@/lib/swal";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { close, loading } from "@/config/swal";
// Types
interface Category {
id: number;
name: string;
}
interface Detail {
id: string;
contentTitle: string;
contentDescription: string;
slug: string;
content: {
id: number;
name: string;
};
contentCreator: string;
creatorName: string;
contentThumbnail: string;
contentTag: string;
categoryId?: number;
contentList?: FileType[];
}
interface Option {
id: string;
label: string;
}
interface FileType {
contentId: number;
contentFile: string;
thumbnailFileUrl: string;
fileName: string;
}
interface PlacementData {
mediaFileId: number;
placements: string;
}
// Schema
const formSchema = z.object({
contentTitle: z.string().min(1, { message: "Judul diperlukan" }),
contentDescription: z.string().optional(),
contentRewriteDescription: z.string().optional(),
contentCreator: z.string().min(1, { message: "Creator diperlukan" }),
});
type FormData = z.infer<typeof formSchema>;
// Constants
const PUBLISH_OPTIONS: Option[] = [
{ id: "all", label: "SEMUA" },
{ id: "5", label: "UMUM" },
{ id: "6", label: "JOURNALIS" },
{ id: "7", label: "POLRI" },
{ id: "8", label: "KSP" },
];
const WRITING_STYLES = [
{ value: "friendly", label: "Friendly" },
{ value: "professional", label: "Profesional" },
{ value: "informational", label: "Informational" },
{ value: "neutral", label: "Neutral" },
{ value: "witty", label: "Witty" },
];
const PLACEMENT_OPTIONS = [
{ value: "all", label: "Semua" },
{ value: "mabes", label: "Nasional" },
{ value: "polda", label: "Wilayah" },
{ value: "international", label: "Internasional" },
];
// Dynamic imports
const CustomEditor = dynamic(
() => import("@/components/editor/custom-editor"),
{
ssr: false,
loading: () => (
<div className="flex items-center justify-center h-32 border rounded-md bg-muted/50">
<Loader2 className="h-6 w-6 animate-spin" />
<span className="ml-2">Loading editor...</span>
</div>
),
}
);
export default function FormConvertSPIT() {
const MySwal = withReactContent(Swal);
const router = useRouter();
const t = useTranslations("Form");
const { id } = useParams() as { id: string };
const [isAlreadySaved, setIsAlreadySaved] = useState(false);
// Form state
const {
control,
handleSubmit,
setValue,
formState: { errors, isSubmitting },
watch,
} = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
contentTitle: "",
contentDescription: "",
contentCreator: "",
contentRewriteDescription: "",
},
});
// Component state
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [detail, setDetail] = useState<Detail | null>(null);
const [categories, setCategories] = useState<Category[]>([]);
const [selectedCategoryId, setSelectedCategoryId] = useState<number | null>(
null
);
const [selectedFileType, setSelectedFileType] = useState<
"original" | "rewrite"
>("original");
const [selectedWritingStyle, setSelectedWritingStyle] =
useState("professional");
const [showRewriteEditor, setShowRewriteEditor] = useState(false);
const [isGeneratingRewrite, setIsGeneratingRewrite] = useState(false);
const [isLoadingRewrite, setIsLoadingRewrite] = useState(false);
const [detailThumb, setDetailThumb] = useState<string[]>([]);
const [thumbsSwiper, setThumbsSwiper] = useState<any>(null);
const [files, setFiles] = useState<FileType[]>([]);
const [filePlacements, setFilePlacements] = useState<string[][]>([]);
const [articleIds, setArticleIds] = useState<string[]>([]);
const [selectedArticleId, setSelectedArticleId] = useState<string | null>(
null
);
const [articleBody, setArticleBody] = useState<string>("");
const [tags, setTags] = useState<string[]>([]);
const [publishedFor, setPublishedFor] = useState<string[]>([]);
const [inputRef] = useState(useRef<HTMLInputElement>(null));
const userLevelId = getCookiesDecrypt("ulie");
const roleId = getCookiesDecrypt("urie");
const [isUserMabesApprover, setIsUserMabesApprover] = useState(false);
useEffect(() => {
initializeComponent();
}, []);
// useEffect(() => {
// const savedFlag = localStorage.getItem(`spit_saved_${id}`);
// if (savedFlag === "true") {
// setIsAlreadySaved(true);
// }
// }, [id]);
useEffect(() => {
checkUserPermissions();
}, [userLevelId, roleId]);
const initializeComponent = async () => {
try {
setIsLoading(true);
await Promise.all([
loadCategories(),
id ? loadDetail() : Promise.resolve(),
]);
} catch (error) {
console.error("Failed to initialize component:", error);
MySwal.fire({
title: "Error",
text: "Failed to load data. Please try again.",
icon: "error",
confirmButtonColor: "#3085d6",
});
} finally {
setIsLoading(false);
}
};
const checkUserPermissions = () => {
if (userLevelId === "216" && roleId === "3") {
setIsUserMabesApprover(true);
}
};
const loadCategories = async () => {
try {
const response = await listEnableCategory("1");
const categories = response?.data?.data?.content || [];
setCategories(categories);
// Auto-select "Pers Rilis" category if schedule type is 3
const scheduleId = Cookies.get("scheduleId");
const scheduleType = Cookies.get("scheduleType");
if (scheduleId && scheduleType === "3") {
const persRilisCategory = categories.find((cat: Category) =>
cat.name.toLowerCase().includes("pers rilis")
);
if (persRilisCategory) {
setSelectedCategoryId(persRilisCategory.id);
await loadTags(persRilisCategory.id);
}
}
} catch (error) {
console.error("Failed to load categories:", error);
}
};
const loadTags = async (categoryId: number) => {
try {
const response = await getTagsBySubCategoryId(categoryId);
if (response?.data?.data?.length > 0) {
const tagsMerge = [...tags, response?.data?.data];
setTags(tagsMerge);
}
} catch (error) {
console.error("Failed to load tags:", error);
}
};
const loadDetail = async () => {
loading();
try {
const response = await detailSPIT(id);
const details = response?.data?.data;
setIsAlreadySaved(details?.isPublish ? true : false);
if (!details) {
throw new Error("Detail not found");
}
setDetail(details);
setFiles(details.contentList || []);
setDetailThumb(
(details.contentList || []).map(
(file: FileType) => file.contentFile || "default-image.jpg"
)
);
// Initialize file placements
const fileCount = details.contentList?.length || 0;
setFilePlacements(Array(fileCount).fill([]));
// Set form values
setValue("contentTitle", details.contentTitle || "");
setValue("contentDescription", details.contentDescription || "");
setValue("contentCreator", details.contentCreator || "");
setValue(
"contentRewriteDescription",
details.contentRewriteDescription || ""
);
if (details.categoryId) {
setSelectedCategoryId(details.categoryId);
await loadTags(details.categoryId);
}
if (details.contentTag) {
const initialTags = details.contentTag
.split(",")
.map((tag: string) => tag.trim());
setTags(initialTags);
}
} catch (error) {
console.error("Failed to load detail:", error);
throw error;
}
close();
};
const handleCategoryChange = async (categoryId: string) => {
const id = Number(categoryId);
setSelectedCategoryId(id);
await loadTags(id);
};
const handleRewriteClick = async () => {
if (!detail?.contentDescription) {
MySwal.fire({
title: "Warning",
text: "Please add content description first",
icon: "warning",
confirmButtonColor: "#3085d6",
});
return;
}
try {
setIsGeneratingRewrite(true);
const request = {
style: selectedWritingStyle,
lang: "id",
contextType: "text",
urlContext: null,
context: detail.contentDescription,
createdBy: roleId,
sentiment: "Humorous",
clientId: "7QTW8cMojyayt6qnhqTOeJaBI70W4EaQ",
};
const response = await generateDataRewrite(request);
if (response?.error) {
throw new Error(response.message);
}
const newArticleId = response?.data?.data?.id;
setArticleIds((prev) => {
const updated = [...prev];
if (updated.length < 3) {
updated.push(newArticleId);
} else {
updated[2] = newArticleId;
}
return updated;
});
setShowRewriteEditor(true);
MySwal.fire({
title: "Success",
text: "Content rewrite generated successfully",
icon: "success",
confirmButtonColor: "#3085d6",
});
} catch (error) {
console.error("Failed to generate rewrite:", error);
MySwal.fire({
title: "Error",
text: "Failed to generate content rewrite",
icon: "error",
confirmButtonColor: "#3085d6",
});
} finally {
setIsGeneratingRewrite(false);
}
};
const handleArticleSelect = async (articleId: string) => {
try {
setIsLoadingRewrite(true);
setSelectedArticleId(articleId);
let retryCount = 0;
const maxRetries = 20;
while (retryCount < maxRetries) {
const response = await getDetailArticle(articleId);
const articleData = response?.data?.data;
if (articleData?.status === 2) {
const cleanArticleBody = articleData.articleBody?.replace(
/<img[^>]*>/g,
""
);
setArticleBody(cleanArticleBody || "");
setValue("contentRewriteDescription", cleanArticleBody || "");
break;
}
retryCount++;
await new Promise((resolve) => setTimeout(resolve, 5000));
}
if (retryCount >= maxRetries) {
throw new Error("Timeout: Article processing took too long");
}
} catch (error) {
console.error("Failed to load article:", error);
MySwal.fire({
title: "Error",
text: "Failed to load article content",
icon: "error",
confirmButtonColor: "#3085d6",
});
} finally {
setIsLoadingRewrite(false);
}
};
const handleAddTag = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && inputRef.current?.value.trim()) {
e.preventDefault();
const newTag = inputRef.current.value.trim();
if (!tags.includes(newTag)) {
setTags((prev) => [...prev, newTag]);
}
inputRef.current.value = "";
}
};
const handleRemoveTag = (index: number) => {
setTags((prev) => prev.filter((_, i) => i !== index));
};
const handlePublishTargetChange = (optionId: string) => {
if (optionId === "all") {
setPublishedFor((prev) =>
prev.length === PUBLISH_OPTIONS.filter((opt) => opt.id !== "all").length
? []
: PUBLISH_OPTIONS.filter((opt) => opt.id !== "all").map(
(opt) => opt.id
)
);
} else {
setPublishedFor((prev) =>
prev.includes(optionId)
? prev.filter((id) => id !== optionId && id !== "all")
: [...prev.filter((id) => id !== "all"), optionId]
);
}
};
const handleFilePlacementChange = (
fileIndex: number,
placement: string,
checked: boolean
) => {
setFilePlacements((prev) => {
const updated = [...prev];
const currentPlacements = updated[fileIndex] || [];
if (checked) {
if (placement === "all") {
updated[fileIndex] = ["all", "mabes", "polda", "international"];
} else {
const newPlacements = [...currentPlacements, placement];
if (newPlacements.length === 3 && !newPlacements.includes("all")) {
newPlacements.push("all");
}
updated[fileIndex] = newPlacements;
}
} else {
if (placement === "all") {
updated[fileIndex] = [];
} else {
const newPlacements = currentPlacements.filter(
(p) => p !== placement
);
if (newPlacements.length === 3 && newPlacements.includes("all")) {
updated[fileIndex] = newPlacements.filter((p) => p !== "all");
} else {
updated[fileIndex] = newPlacements;
}
}
}
return updated;
});
};
const handleSelectAllPlacements = (placement: string, checked: boolean) => {
setFilePlacements((prev) =>
prev.map((filePlacements) =>
checked
? Array.from(new Set([...filePlacements, placement]))
: filePlacements.filter((p) => p !== placement)
)
);
};
const getPlacementData = () => {
const placementData = [];
console.log("filePlacements : ", filePlacements);
for (let i = 0; i < filePlacements.length; i++) {
if (filePlacements[i].length > 0) {
const placements = filePlacements[i];
placementData.push({
mediaFileId: files[i].contentId,
placements: placements.join(","),
});
}
}
return placementData;
};
const checkPlacement = (data: any) => {
let temp = true;
for (const element of data) {
if (element.length < 1) {
temp = false;
break;
}
}
return temp;
};
// Form submission
const onSubmit = async (data: FormData) => {
const pnmhTags = tags.filter((tag) => tag.toLowerCase().includes("pnmh"));
if (pnmhTags.length > 1) {
MySwal.fire({
title: "Error",
text: "Tags penugasan hanya diperbolehkan 1 (satu) saja.",
icon: "error",
confirmButtonColor: "#3085d6",
});
return false;
}
if (!checkPlacement(filePlacements)) {
error("Select File Placement");
return false;
}
if (!selectedCategoryId) {
error("Select a category");
return false;
}
if (publishedFor.length < 1) {
error("Select Publish Target");
return false;
}
try {
setIsSaving(true);
const description =
selectedFileType === "original"
? data.contentDescription
: data.contentRewriteDescription;
const requestData = {
spitId: id,
title: data.contentTitle,
description,
htmlDescription: description,
tags: tags.join(", "),
categoryId: selectedCategoryId,
publishedFor: publishedFor.join(","),
creator: data.contentCreator,
files: isUserMabesApprover ? getPlacementData() : [],
};
await convertSPIT(requestData);
// localStorage.setItem(`spit_saved_${id}`, "true");
// setIsAlreadySaved(true);
MySwal.fire({
title: "Success",
text: "Data saved successfully",
icon: "success",
confirmButtonColor: "#3085d6",
}).then(() => {
// router.push("/in/contributor/content/spit");
// router.replace(`${window.location.pathname}?id=${id}`);
loadDetail();
});
} catch (error) {
console.error("Failed to save:", error);
MySwal.fire({
title: "Error",
text: "Failed to save data",
icon: "error",
confirmButtonColor: "#3085d6",
});
} finally {
setIsSaving(false);
}
};
const handleDelete = async () => {
const result = await MySwal.fire({
title: "Delete Content",
text: "Are you sure you want to delete this content?",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#dc3545",
cancelButtonColor: "#6c757d",
confirmButtonText: "Delete",
cancelButtonText: "Cancel",
});
if (result.isConfirmed) {
try {
setIsDeleting(true);
await deleteSPIT(id);
MySwal.fire({
title: "Success",
text: "Content deleted successfully",
icon: "success",
confirmButtonColor: "#3085d6",
}).then(() => {
router.back();
});
} catch (error) {
console.error("Failed to delete:", error);
MySwal.fire({
title: "Error",
text: "Failed to delete content",
icon: "error",
confirmButtonColor: "#3085d6",
});
} finally {
setIsDeleting(false);
}
}
};
// Loading state
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<Loader2 className="h-8 w-8 animate-spin mx-auto mb-4" />
<p className="text-muted-foreground">Loading form data...</p>
</div>
</div>
);
}
return (
<div className="mx-auto py-6 space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">
SPIT Convert Form
</h1>
<p className="text-muted-foreground">
Convert and manage your SPIT content
</p>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={() => router.back()}>
<XCircle className="h-4 w-4 mr-2" />
Cancel
</Button>
<Button
variant="default"
color="destructive"
size="sm"
onClick={handleDelete}
disabled={isDeleting}
>
{isDeleting ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Trash2 className="h-4 w-4 mr-2" />
)}
Delete
</Button>
</div>
</div>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main Content */}
<div className="lg:col-span-2 space-y-6">
{/* Basic Information */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileText className="h-5 w-5" />
Basic Information
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Title */}
<div className="space-y-2">
<Label htmlFor="title">Title *</Label>
<Controller
control={control}
name="contentTitle"
render={({ field }) => (
<Input
id="title"
placeholder="Enter content title"
{...field}
/>
)}
/>
{errors.contentTitle && (
<Alert variant="soft">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
{errors.contentTitle.message}
</AlertDescription>
</Alert>
)}
</div>
{/* Category */}
<div className="space-y-2">
<Label htmlFor="category">Category</Label>
<Select
value={selectedCategoryId?.toString()}
onValueChange={handleCategoryChange}
>
<SelectTrigger>
<SelectValue placeholder="Select category" />
</SelectTrigger>
<SelectContent>
{categories.map((category) => (
<SelectItem
key={category.id}
value={category.id.toString()}
>
{category.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{/* Content Editor */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Edit3 className="h-5 w-5" />
Content Editor
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<RadioGroup
value={selectedFileType}
onValueChange={(value: "original" | "rewrite") =>
setSelectedFileType(value)
}
className="grid grid-cols-2 gap-4"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="original" id="original" />
<Label htmlFor="original">Original Content</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="rewrite" id="rewrite" />
<Label htmlFor="rewrite">Rewritten Content</Label>
</div>
</RadioGroup>
{/* Original Content */}
{selectedFileType === "original" && (
<div className="space-y-2">
<Label>Content Description</Label>
<Controller
control={control}
name="contentDescription"
render={({ field }) => (
<CustomEditor
onChange={field.onChange}
initialData={field.value}
/>
)}
/>
</div>
)}
{/* Content Rewrite */}
{selectedFileType === "rewrite" && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-2">
<Label>Writing Style</Label>
<Select
value={selectedWritingStyle}
onValueChange={setSelectedWritingStyle}
>
<SelectTrigger className="w-48">
<SelectValue />
</SelectTrigger>
<SelectContent>
{WRITING_STYLES.map((style) => (
<SelectItem key={style.value} value={style.value}>
{style.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button
type="button"
onClick={handleRewriteClick}
disabled={
isGeneratingRewrite || !detail?.contentDescription
}
className="bg-blue-600 hover:bg-blue-700"
>
{isGeneratingRewrite ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Edit3 className="h-4 w-4 mr-2" />
)}
Generate Rewrite
</Button>
</div>
{showRewriteEditor && (
<div className="space-y-4">
{articleIds.length > 0 && (
<div className="flex gap-2">
{articleIds.map((articleId, index) => (
<Button
key={articleId}
type="button"
variant={
selectedArticleId === articleId
? "default"
: "outline"
}
size="sm"
onClick={() => handleArticleSelect(articleId)}
disabled={isLoadingRewrite}
>
{isLoadingRewrite &&
selectedArticleId === articleId && (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
)}
Narrative {index + 1}
</Button>
))}
</div>
)}
<div className="space-y-2">
<Label>Rewritten Content</Label>
<Controller
control={control}
name="contentRewriteDescription"
render={({ field }) => (
<CustomEditor
onChange={field.onChange}
initialData={articleBody || field.value}
/>
)}
/>
</div>
</div>
)}
</div>
)}
</CardContent>
</Card>
{/* Media Files */}
{detailThumb.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Image className="h-5 w-5" />
Media Files
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-4">
<Swiper
thumbs={{ swiper: thumbsSwiper }}
modules={[FreeMode, Navigation, Thumbs]}
navigation={true}
className="w-full h-96"
>
{detailThumb.map((image, index) => (
<SwiperSlide key={index}>
<img
src={image}
alt={`Media ${index + 1}`}
className="w-full h-full object-cover rounded-lg"
/>
</SwiperSlide>
))}
</Swiper>
<Swiper
onSwiper={setThumbsSwiper}
slidesPerView={8}
spaceBetween={8}
modules={[Pagination, Thumbs]}
className="w-full"
>
{detailThumb.map((image, index) => (
<SwiperSlide key={index}>
<img
src={image}
alt={`Thumbnail ${index + 1}`}
className="w-full h-16 object-cover rounded cursor-pointer"
/>
</SwiperSlide>
))}
</Swiper>
</div>
</CardContent>
</Card>
)}
{/* File Placement */}
{files.length > 0 && isUserMabesApprover && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Globe className="h-5 w-5" />
File Placement
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{files.length > 1 && (
<div className="flex flex-wrap gap-4 p-4 bg-muted/50 rounded-lg">
{PLACEMENT_OPTIONS.map((option) => (
<div
key={option.value}
className="flex items-center space-x-2"
>
<Checkbox
id={`select-all-${option.value}`}
onCheckedChange={(checked) =>
handleSelectAllPlacements(
option.value,
Boolean(checked)
)
}
/>
<Label
htmlFor={`select-all-${option.value}`}
className="text-sm"
>
All {option.label}
</Label>
</div>
))}
</div>
)}
<div className="space-y-4">
{files.map((file, index) => (
<div
key={file.contentId}
className="flex gap-4 p-4 border rounded-lg"
>
<img
src={file.contentFile}
alt={file.fileName}
className="w-32 h-24 object-cover rounded"
/>
<div className="flex-1 space-y-3">
<p className="font-medium text-sm">{file.fileName}</p>
<div className="flex flex-wrap gap-3">
{PLACEMENT_OPTIONS.map((option) => (
<div
key={option.value}
className="flex items-center space-x-2"
>
<Checkbox
id={`${file.contentId}-${option.value}`}
checked={filePlacements[index]?.includes(
option.value
)}
onCheckedChange={(checked) =>
handleFilePlacementChange(
index,
option.value,
Boolean(checked)
)
}
/>
<Label
htmlFor={`${file.contentId}-${option.value}`}
className="text-sm"
>
{option.label}
</Label>
</div>
))}
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
)}
</div>
{/* Sidebar */}
<div className="space-y-6">
{/* Creator Information */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Users className="h-5 w-5" />
Creator Information
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="creator">Creator *</Label>
<Controller
control={control}
name="contentCreator"
render={({ field }) => (
<Input
id="creator"
placeholder="Enter creator name"
{...field}
/>
)}
/>
{errors.contentCreator && (
<Alert variant="soft">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
{errors.contentCreator.message}
</AlertDescription>
</Alert>
)}
</div>
</CardContent>
</Card>
{/* Preview */}
{detail?.contentThumbnail && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Eye className="h-5 w-5" />
Preview
</CardTitle>
</CardHeader>
<CardContent>
<img
src={detail.contentThumbnail}
alt="Content thumbnail"
className="w-full h-auto rounded-lg"
/>
</CardContent>
</Card>
)}
{/* Tags */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Tag className="h-5 w-5" />
Tags
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="tag-input">Add Tags</Label>
<Input
id="tag-input"
placeholder="Type a tag and press Enter"
onKeyDown={handleAddTag}
ref={inputRef}
/>
</div>
{tags.length > 0 && (
<div className="flex flex-wrap gap-2">
{tags.map((tag, index) => (
<Badge
key={index}
className="cursor-pointer hover:bg-destructive hover:text-destructive-foreground"
onClick={() => handleRemoveTag(index)}
>
{tag}
<XCircle className="h-3 w-3 ml-1" />
</Badge>
))}
</div>
)}
</CardContent>
</Card>
{/* Publish Targets */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Settings className="h-5 w-5" />
Publish Targets
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{PUBLISH_OPTIONS.map((option) => (
<div key={option.id} className="flex items-center space-x-2">
<Checkbox
id={option.id}
checked={
option.id === "all"
? publishedFor.length ===
PUBLISH_OPTIONS.filter((opt) => opt.id !== "all")
.length
: publishedFor.includes(option.id)
}
onCheckedChange={() =>
handlePublishTargetChange(option.id)
}
/>
<Label htmlFor={option.id} className="text-sm">
{option.label}
</Label>
</div>
))}
</CardContent>
</Card>
{/* Submit Button */}
<Card>
<CardContent className="pt-6">
<Button
type="submit"
className="w-full mb-4"
disabled={isSubmitting || isSaving || isAlreadySaved}
>
{isSubmitting || isSaving ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Save className="h-4 w-4 mr-2" />
)}
{isAlreadySaved
? "Already Saved"
: isSubmitting || isSaving
? "Saving..."
: "Save Changes"}
</Button>
{isAlreadySaved && (
<Alert variant="soft">
<CheckCircle className="h-4 w-4 text-red-500" />
<AlertDescription className="text-red-500">
Konten sudah disimpan. Anda tidak dapat menyimpan ulang.
</AlertDescription>
</Alert>
)}
</CardContent>
</Card>
</div>
</div>
</form>
</div>
);
}