fix:handle empty file placement & target publish spit

This commit is contained in:
Rama Priyanto 2025-07-22 09:29:16 +07:00
parent 933bbc402b
commit 504101b91c
1 changed files with 158 additions and 86 deletions

View File

@ -28,13 +28,13 @@ import { Checkbox } from "@/components/ui/checkbox";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
// Icons // Icons
import { import {
AlertCircle, AlertCircle,
FileText, FileText,
Image, Image,
Loader2, Loader2,
Save, Save,
Trash2, Trash2,
Edit3, Edit3,
Globe, Globe,
Users, Users,
@ -42,7 +42,7 @@ import {
Eye, Eye,
Settings, Settings,
CheckCircle, CheckCircle,
XCircle XCircle,
} from "lucide-react"; } from "lucide-react";
// Swiper // Swiper
@ -62,10 +62,7 @@ import {
getTagsBySubCategoryId, getTagsBySubCategoryId,
listEnableCategory, listEnableCategory,
} from "@/service/content/content"; } from "@/service/content/content";
import { import { generateDataRewrite, getDetailArticle } from "@/service/content/ai";
generateDataRewrite,
getDetailArticle,
} from "@/service/content/ai";
// Utils // Utils
import { getCookiesDecrypt } from "@/lib/utils"; import { getCookiesDecrypt } from "@/lib/utils";
@ -150,14 +147,14 @@ const PLACEMENT_OPTIONS = [
// Dynamic imports // Dynamic imports
const CustomEditor = dynamic( const CustomEditor = dynamic(
() => import("@/components/editor/custom-editor"), () => import("@/components/editor/custom-editor"),
{ {
ssr: false, ssr: false,
loading: () => ( loading: () => (
<div className="flex items-center justify-center h-32 border rounded-md bg-muted/50"> <div className="flex items-center justify-center h-32 border rounded-md bg-muted/50">
<Loader2 className="h-6 w-6 animate-spin" /> <Loader2 className="h-6 w-6 animate-spin" />
<span className="ml-2">Loading editor...</span> <span className="ml-2">Loading editor...</span>
</div> </div>
) ),
} }
); );
@ -190,24 +187,31 @@ export default function FormConvertSPIT() {
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
const [detail, setDetail] = useState<Detail | null>(null); const [detail, setDetail] = useState<Detail | null>(null);
const [categories, setCategories] = useState<Category[]>([]); const [categories, setCategories] = useState<Category[]>([]);
const [selectedCategoryId, setSelectedCategoryId] = useState<number | null>(null); const [selectedCategoryId, setSelectedCategoryId] = useState<number | null>(
const [selectedFileType, setSelectedFileType] = useState<"original" | "rewrite">("original"); null
const [selectedWritingStyle, setSelectedWritingStyle] = useState("professional"); );
const [selectedFileType, setSelectedFileType] = useState<
"original" | "rewrite"
>("original");
const [selectedWritingStyle, setSelectedWritingStyle] =
useState("professional");
const [showRewriteEditor, setShowRewriteEditor] = useState(false); const [showRewriteEditor, setShowRewriteEditor] = useState(false);
const [isGeneratingRewrite, setIsGeneratingRewrite] = useState(false); const [isGeneratingRewrite, setIsGeneratingRewrite] = useState(false);
const [isLoadingRewrite, setIsLoadingRewrite] = useState(false); const [isLoadingRewrite, setIsLoadingRewrite] = useState(false);
// Media state // Media state
const [detailThumb, setDetailThumb] = useState<string[]>([]); const [detailThumb, setDetailThumb] = useState<string[]>([]);
const [thumbsSwiper, setThumbsSwiper] = useState<any>(null); const [thumbsSwiper, setThumbsSwiper] = useState<any>(null);
const [files, setFiles] = useState<FileType[]>([]); const [files, setFiles] = useState<FileType[]>([]);
const [filePlacements, setFilePlacements] = useState<string[][]>([]); const [filePlacements, setFilePlacements] = useState<string[][]>([]);
// Content rewrite state // Content rewrite state
const [articleIds, setArticleIds] = useState<string[]>([]); const [articleIds, setArticleIds] = useState<string[]>([]);
const [selectedArticleId, setSelectedArticleId] = useState<string | null>(null); const [selectedArticleId, setSelectedArticleId] = useState<string | null>(
null
);
const [articleBody, setArticleBody] = useState<string>(""); const [articleBody, setArticleBody] = useState<string>("");
// Form data state // Form data state
const [tags, setTags] = useState<string[]>([]); const [tags, setTags] = useState<string[]>([]);
const [publishedFor, setPublishedFor] = useState<string[]>([]); const [publishedFor, setPublishedFor] = useState<string[]>([]);
@ -262,7 +266,7 @@ export default function FormConvertSPIT() {
// Auto-select "Pers Rilis" category if schedule type is 3 // Auto-select "Pers Rilis" category if schedule type is 3
const scheduleId = Cookies.get("scheduleId"); const scheduleId = Cookies.get("scheduleId");
const scheduleType = Cookies.get("scheduleType"); const scheduleType = Cookies.get("scheduleType");
if (scheduleId && scheduleType === "3") { if (scheduleId && scheduleType === "3") {
const persRilisCategory = categories.find((cat: Category) => const persRilisCategory = categories.find((cat: Category) =>
cat.name.toLowerCase().includes("pers rilis") cat.name.toLowerCase().includes("pers rilis")
@ -281,7 +285,7 @@ export default function FormConvertSPIT() {
try { try {
const response = await getTagsBySubCategoryId(categoryId); const response = await getTagsBySubCategoryId(categoryId);
if (response?.data?.data?.length > 0) { if (response?.data?.data?.length > 0) {
const tagsMerge = [...tags, response?.data?.data] const tagsMerge = [...tags, response?.data?.data];
setTags(tagsMerge); setTags(tagsMerge);
} }
} catch (error) { } catch (error) {
@ -293,7 +297,7 @@ export default function FormConvertSPIT() {
try { try {
const response = await detailSPIT(id); const response = await detailSPIT(id);
const details = response?.data?.data; const details = response?.data?.data;
if (!details) { if (!details) {
throw new Error("Detail not found"); throw new Error("Detail not found");
} }
@ -301,11 +305,11 @@ export default function FormConvertSPIT() {
setDetail(details); setDetail(details);
setFiles(details.contentList || []); setFiles(details.contentList || []);
setDetailThumb( setDetailThumb(
(details.contentList || []).map((file: FileType) => (details.contentList || []).map(
file.contentFile || "default-image.jpg" (file: FileType) => file.contentFile || "default-image.jpg"
) )
); );
// Initialize file placements // Initialize file placements
const fileCount = details.contentList?.length || 0; const fileCount = details.contentList?.length || 0;
setFilePlacements(Array(fileCount).fill([])); setFilePlacements(Array(fileCount).fill([]));
@ -314,7 +318,10 @@ export default function FormConvertSPIT() {
setValue("contentTitle", details.contentTitle || ""); setValue("contentTitle", details.contentTitle || "");
setValue("contentDescription", details.contentDescription || ""); setValue("contentDescription", details.contentDescription || "");
setValue("contentCreator", details.contentCreator || ""); setValue("contentCreator", details.contentCreator || "");
setValue("contentRewriteDescription", details.contentRewriteDescription || ""); setValue(
"contentRewriteDescription",
details.contentRewriteDescription || ""
);
// Set category // Set category
if (details.categoryId) { if (details.categoryId) {
@ -324,7 +331,9 @@ export default function FormConvertSPIT() {
// Set tags // Set tags
if (details.contentTag) { if (details.contentTag) {
const initialTags = details.contentTag.split(",").map((tag: string) => tag.trim()); const initialTags = details.contentTag
.split(",")
.map((tag: string) => tag.trim());
setTags(initialTags); setTags(initialTags);
} }
} catch (error) { } catch (error) {
@ -365,13 +374,13 @@ export default function FormConvertSPIT() {
}; };
const response = await generateDataRewrite(request); const response = await generateDataRewrite(request);
if (response?.error) { if (response?.error) {
throw new Error(response.message); throw new Error(response.message);
} }
const newArticleId = response?.data?.data?.id; const newArticleId = response?.data?.data?.id;
setArticleIds(prev => { setArticleIds((prev) => {
const updated = [...prev]; const updated = [...prev];
if (updated.length < 3) { if (updated.length < 3) {
updated.push(newArticleId); updated.push(newArticleId);
@ -405,23 +414,26 @@ export default function FormConvertSPIT() {
try { try {
setIsLoadingRewrite(true); setIsLoadingRewrite(true);
setSelectedArticleId(articleId); setSelectedArticleId(articleId);
let retryCount = 0; let retryCount = 0;
const maxRetries = 20; const maxRetries = 20;
while (retryCount < maxRetries) { while (retryCount < maxRetries) {
const response = await getDetailArticle(articleId); const response = await getDetailArticle(articleId);
const articleData = response?.data?.data; const articleData = response?.data?.data;
if (articleData?.status === 2) { if (articleData?.status === 2) {
const cleanArticleBody = articleData.articleBody?.replace(/<img[^>]*>/g, ""); const cleanArticleBody = articleData.articleBody?.replace(
/<img[^>]*>/g,
""
);
setArticleBody(cleanArticleBody || ""); setArticleBody(cleanArticleBody || "");
setValue("contentRewriteDescription", cleanArticleBody || ""); setValue("contentRewriteDescription", cleanArticleBody || "");
break; break;
} }
retryCount++; retryCount++;
await new Promise(resolve => setTimeout(resolve, 5000)); await new Promise((resolve) => setTimeout(resolve, 5000));
} }
if (retryCount >= maxRetries) { if (retryCount >= maxRetries) {
@ -444,40 +456,46 @@ export default function FormConvertSPIT() {
if (e.key === "Enter" && inputRef.current?.value.trim()) { if (e.key === "Enter" && inputRef.current?.value.trim()) {
e.preventDefault(); e.preventDefault();
const newTag = inputRef.current.value.trim(); const newTag = inputRef.current.value.trim();
if (!tags.includes(newTag)) { if (!tags.includes(newTag)) {
setTags(prev => [...prev, newTag]); setTags((prev) => [...prev, newTag]);
} }
inputRef.current.value = ""; inputRef.current.value = "";
} }
}; };
const handleRemoveTag = (index: number) => { const handleRemoveTag = (index: number) => {
setTags(prev => prev.filter((_, i) => i !== index)); setTags((prev) => prev.filter((_, i) => i !== index));
}; };
const handlePublishTargetChange = (optionId: string) => { const handlePublishTargetChange = (optionId: string) => {
if (optionId === "all") { if (optionId === "all") {
setPublishedFor(prev => setPublishedFor((prev) =>
prev.length === PUBLISH_OPTIONS.filter(opt => opt.id !== "all").length prev.length === PUBLISH_OPTIONS.filter((opt) => opt.id !== "all").length
? [] ? []
: PUBLISH_OPTIONS.filter(opt => opt.id !== "all").map(opt => opt.id) : PUBLISH_OPTIONS.filter((opt) => opt.id !== "all").map(
(opt) => opt.id
)
); );
} else { } else {
setPublishedFor(prev => setPublishedFor((prev) =>
prev.includes(optionId) prev.includes(optionId)
? prev.filter(id => id !== optionId && id !== "all") ? prev.filter((id) => id !== optionId && id !== "all")
: [...prev.filter(id => id !== "all"), optionId] : [...prev.filter((id) => id !== "all"), optionId]
); );
} }
}; };
const handleFilePlacementChange = (fileIndex: number, placement: string, checked: boolean) => { const handleFilePlacementChange = (
setFilePlacements(prev => { fileIndex: number,
placement: string,
checked: boolean
) => {
setFilePlacements((prev) => {
const updated = [...prev]; const updated = [...prev];
const currentPlacements = updated[fileIndex] || []; const currentPlacements = updated[fileIndex] || [];
if (checked) { if (checked) {
if (placement === "all") { if (placement === "all") {
updated[fileIndex] = ["all", "mabes", "polda", "international"]; updated[fileIndex] = ["all", "mabes", "polda", "international"];
@ -492,25 +510,27 @@ export default function FormConvertSPIT() {
if (placement === "all") { if (placement === "all") {
updated[fileIndex] = []; updated[fileIndex] = [];
} else { } else {
const newPlacements = currentPlacements.filter(p => p !== placement); const newPlacements = currentPlacements.filter(
(p) => p !== placement
);
if (newPlacements.length === 3 && newPlacements.includes("all")) { if (newPlacements.length === 3 && newPlacements.includes("all")) {
updated[fileIndex] = newPlacements.filter(p => p !== "all"); updated[fileIndex] = newPlacements.filter((p) => p !== "all");
} else { } else {
updated[fileIndex] = newPlacements; updated[fileIndex] = newPlacements;
} }
} }
} }
return updated; return updated;
}); });
}; };
const handleSelectAllPlacements = (placement: string, checked: boolean) => { const handleSelectAllPlacements = (placement: string, checked: boolean) => {
setFilePlacements(prev => setFilePlacements((prev) =>
prev.map(filePlacements => prev.map((filePlacements) =>
checked checked
? Array.from(new Set([...filePlacements, placement])) ? Array.from(new Set([...filePlacements, placement]))
: filePlacements.filter(p => p !== placement) : filePlacements.filter((p) => p !== placement)
) )
); );
}; };
@ -520,7 +540,7 @@ export default function FormConvertSPIT() {
console.log("filePlacements : ", filePlacements); console.log("filePlacements : ", filePlacements);
for (let i = 0; i < filePlacements.length; i++) { for (let i = 0; i < filePlacements.length; i++) {
if (filePlacements[i].length > 0) { if (filePlacements[i].length > 0) {
const placements = filePlacements[i]; const placements = filePlacements[i];
placementData.push({ placementData.push({
mediaFileId: files[i].contentId, mediaFileId: files[i].contentId,
placements: placements.join(","), placements: placements.join(","),
@ -530,15 +550,38 @@ export default function FormConvertSPIT() {
return placementData; 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 // Form submission
const onSubmit = async (data: FormData) => { const onSubmit = async (data: FormData) => {
if (!checkPlacement(filePlacements)) {
error("Select File Placement");
return false;
}
if (publishedFor.length < 1) {
error("Select Publish Target");
return false;
}
try { try {
setIsSaving(true); setIsSaving(true);
const description = selectedFileType === "original" const description =
? data.contentDescription selectedFileType === "original"
: data.contentRewriteDescription; ? data.contentDescription
: data.contentRewriteDescription;
const requestData = { const requestData = {
spitId: id, spitId: id,
title: data.contentTitle, title: data.contentTitle,
@ -552,7 +595,7 @@ export default function FormConvertSPIT() {
}; };
await convertSPIT(requestData); await convertSPIT(requestData);
MySwal.fire({ MySwal.fire({
title: "Success", title: "Success",
text: "Data saved successfully", text: "Data saved successfully",
@ -590,7 +633,7 @@ export default function FormConvertSPIT() {
try { try {
setIsDeleting(true); setIsDeleting(true);
await deleteSPIT(id); await deleteSPIT(id);
MySwal.fire({ MySwal.fire({
title: "Success", title: "Success",
text: "Content deleted successfully", text: "Content deleted successfully",
@ -630,17 +673,15 @@ export default function FormConvertSPIT() {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold tracking-tight">SPIT Convert Form</h1> <h1 className="text-3xl font-bold tracking-tight">
SPIT Convert Form
</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Convert and manage your SPIT content Convert and manage your SPIT content
</p> </p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button variant="outline" size="sm" onClick={() => router.back()}>
variant="outline"
size="sm"
onClick={() => router.back()}
>
<XCircle className="h-4 w-4 mr-2" /> <XCircle className="h-4 w-4 mr-2" />
Cancel Cancel
</Button> </Button>
@ -734,7 +775,9 @@ export default function FormConvertSPIT() {
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<RadioGroup <RadioGroup
value={selectedFileType} value={selectedFileType}
onValueChange={(value: "original" | "rewrite") => setSelectedFileType(value)} onValueChange={(value: "original" | "rewrite") =>
setSelectedFileType(value)
}
className="grid grid-cols-2 gap-4" className="grid grid-cols-2 gap-4"
> >
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
@ -789,7 +832,9 @@ export default function FormConvertSPIT() {
<Button <Button
type="button" type="button"
onClick={handleRewriteClick} onClick={handleRewriteClick}
disabled={isGeneratingRewrite || !detail?.contentDescription} disabled={
isGeneratingRewrite || !detail?.contentDescription
}
className="bg-blue-600 hover:bg-blue-700" className="bg-blue-600 hover:bg-blue-700"
> >
{isGeneratingRewrite ? ( {isGeneratingRewrite ? (
@ -809,14 +854,19 @@ export default function FormConvertSPIT() {
<Button <Button
key={articleId} key={articleId}
type="button" type="button"
variant={selectedArticleId === articleId ? "default" : "outline"} variant={
selectedArticleId === articleId
? "default"
: "outline"
}
size="sm" size="sm"
onClick={() => handleArticleSelect(articleId)} onClick={() => handleArticleSelect(articleId)}
disabled={isLoadingRewrite} disabled={isLoadingRewrite}
> >
{isLoadingRewrite && selectedArticleId === articleId && ( {isLoadingRewrite &&
<Loader2 className="h-4 w-4 mr-2 animate-spin" /> selectedArticleId === articleId && (
)} <Loader2 className="h-4 w-4 mr-2 animate-spin" />
)}
Narrative {index + 1} Narrative {index + 1}
</Button> </Button>
))} ))}
@ -870,7 +920,7 @@ export default function FormConvertSPIT() {
</SwiperSlide> </SwiperSlide>
))} ))}
</Swiper> </Swiper>
<Swiper <Swiper
onSwiper={setThumbsSwiper} onSwiper={setThumbsSwiper}
slidesPerView={8} slidesPerView={8}
@ -906,14 +956,23 @@ export default function FormConvertSPIT() {
{files.length > 1 && ( {files.length > 1 && (
<div className="flex flex-wrap gap-4 p-4 bg-muted/50 rounded-lg"> <div className="flex flex-wrap gap-4 p-4 bg-muted/50 rounded-lg">
{PLACEMENT_OPTIONS.map((option) => ( {PLACEMENT_OPTIONS.map((option) => (
<div key={option.value} className="flex items-center space-x-2"> <div
key={option.value}
className="flex items-center space-x-2"
>
<Checkbox <Checkbox
id={`select-all-${option.value}`} id={`select-all-${option.value}`}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
handleSelectAllPlacements(option.value, Boolean(checked)) handleSelectAllPlacements(
option.value,
Boolean(checked)
)
} }
/> />
<Label htmlFor={`select-all-${option.value}`} className="text-sm"> <Label
htmlFor={`select-all-${option.value}`}
className="text-sm"
>
All {option.label} All {option.label}
</Label> </Label>
</div> </div>
@ -936,12 +995,21 @@ export default function FormConvertSPIT() {
<p className="font-medium text-sm">{file.fileName}</p> <p className="font-medium text-sm">{file.fileName}</p>
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
{PLACEMENT_OPTIONS.map((option) => ( {PLACEMENT_OPTIONS.map((option) => (
<div key={option.value} className="flex items-center space-x-2"> <div
key={option.value}
className="flex items-center space-x-2"
>
<Checkbox <Checkbox
id={`${file.contentId}-${option.value}`} id={`${file.contentId}-${option.value}`}
checked={filePlacements[index]?.includes(option.value)} checked={filePlacements[index]?.includes(
option.value
)}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
handleFilePlacementChange(index, option.value, Boolean(checked)) handleFilePlacementChange(
index,
option.value,
Boolean(checked)
)
} }
/> />
<Label <Label
@ -1067,10 +1135,14 @@ export default function FormConvertSPIT() {
id={option.id} id={option.id}
checked={ checked={
option.id === "all" option.id === "all"
? publishedFor.length === PUBLISH_OPTIONS.filter(opt => opt.id !== "all").length ? publishedFor.length ===
PUBLISH_OPTIONS.filter((opt) => opt.id !== "all")
.length
: publishedFor.includes(option.id) : publishedFor.includes(option.id)
} }
onCheckedChange={() => handlePublishTargetChange(option.id)} onCheckedChange={() =>
handlePublishTargetChange(option.id)
}
/> />
<Label htmlFor={option.id} className="text-sm"> <Label htmlFor={option.id} className="text-sm">
{option.label} {option.label}