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

2599 lines
88 KiB
TypeScript

"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[]; // keep as FileType[]
}
interface Option {
id: string;
label: string;
}
// Updated FileType to reflect API response
interface FileType {
contentId: number;
contentFile: string; // URL
contentType: "IMAGE" | "VIDEO" | string;
contentFileRawType?: string; // e.g. video/mp4 or image/jpeg
contentFileName?: string | null;
thumbnailFileUrl?: string | null;
fileName?: string | null;
}
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);
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);
// <-- changed: detailThumb is now FileType[] (objects from API) -->
const [detailThumb, setDetailThumb] = useState<FileType[]>([]);
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(() => {
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);
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);
// <-- use API contentList objects directly -->
const contentList: FileType[] = (details.contentList || []).map(
(it: any) => ({
contentId: it.contentId,
contentFile: it.contentFile,
contentType: it.contentType,
contentFileRawType: it.contentFileRawType,
contentFileName: it.contentFileName,
thumbnailFileUrl: it.thumbnailFileUrl || null,
fileName: it.contentFileName || null,
})
);
setFiles(contentList);
setDetailThumb(contentList);
// Initialize file placements
const fileCount = 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 as any).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: 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);
MySwal.fire({
title: "Success",
text: "Data saved successfully",
icon: "success",
confirmButtonColor: "#3085d6",
}).then(() => {
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-8">
{/* Original Content */}
<div className="space-y-2">
<Label className="text-lg text-black">Original Content</Label>
<Controller
control={control}
name="contentDescription"
render={({ field }) => (
<CustomEditor
onChange={field.onChange}
initialData={field.value}
/>
)}
/>
</div>
{/* Content Rewrite */}
<div className="space-y-4">
<Label className="text-lg text-black">
Rewritten Content
</Label>
<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 placeholder="Select style" />
</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>
{/* <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((item) => (
<SwiperSlide key={item.contentId}>
{item.contentType === "VIDEO" ? (
<div className="relative max-h-screen overflow-hidden">
<div className="w-full max-h-screen aspect-video">
<div className="w-full h-full object-contain">
{/* main video player */}
<video
className="object-contain h-full w-full rounded-lg"
src={item.contentFile}
controls
// playsInline to better on mobile
playsInline
// you can set poster if available: poster={item.thumbnailFileUrl}
title={`Video ${item.contentId}`}
/>
</div>
</div>
</div>
) : (
<img
src={item.contentFile}
alt={`Media ${item.contentId}`}
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((item) => (
<SwiperSlide key={`thumb-${item.contentId}`}>
{item.contentType === "VIDEO" ? (
<div className="relative w-full h-16 rounded cursor-pointer overflow-hidden">
{/* use preload metadata so browser doesn't download full video */}
<video
src={item.contentFile}
className="w-full h-16 object-cover"
muted
preload="metadata"
playsInline
// no controls in thumbnail
tabIndex={-1}
/>
<div className="absolute inset-0 flex items-center justify-center bg-black/30 pointer-events-none">
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-6 h-6 text-white"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M8 5v14l11-7z" />
</svg>
</div>
</div>
) : (
<img
src={item.contentFile}
alt={`Thumbnail ${item.contentId}`}
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"
>
{/* show thumbnail or video preview */}
{file.contentType === "VIDEO" ? (
<video
src={file.contentFile}
className="w-32 h-24 object-cover rounded"
muted
preload="metadata"
playsInline
/>
) : (
<img
src={file.contentFile}
alt={file.fileName || `file-${file.contentId}`}
className="w-32 h-24 object-cover rounded"
/>
)}
<div className="flex-1 space-y-3">
<p className="font-medium text-sm">
{file.fileName ||
file.contentFileName ||
`File ${file.contentId}`}
</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>
);
}
// "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>
// );
// }