This commit is contained in:
Sabda Yagra 2025-07-19 01:06:24 +07:00
parent 00ddca22b9
commit 978c8b364f
6 changed files with 583 additions and 508 deletions

View File

@ -48,12 +48,12 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ urlAudio, fileName }) => {
<div className="mt-2"> <div className="mt-2">
<h2 className="text-lg font-semibold">{fileName}</h2> <h2 className="text-lg font-semibold">{fileName}</h2>
<audio ref={audioRef} src={urlAudio} controls className="mt-1 w-full" /> <audio ref={audioRef} src={urlAudio} controls className="mt-1 w-full" />
{/* <div className="mt-2 space-x-2"> <div className="mt-2 space-x-2">
<button onClick={playAudio}> Play</button> <button onClick={playAudio}> Play</button>
<button onClick={pauseAudio}> Pause</button> <button onClick={pauseAudio}> Pause</button>
<button onClick={stopAudio}> Stop</button> <button onClick={stopAudio}> Stop</button>
</div> </div>
<div className="mt-1 text-sm text-gray-500">{formatTime(currentTime)}</div> */} <div className="mt-1 text-sm text-gray-500">{formatTime(currentTime)}</div>
</div> </div>
); );
}; };

View File

@ -290,7 +290,6 @@ export default function FormAudioDetail() {
setSelectedPublishers(publisherIds); setSelectedPublishers(publisherIds);
} }
// Set the selected target to the category ID from details
setSelectedTarget(String(details.category.id)); setSelectedTarget(String(details.category.id));
const filesData = details?.files || []; const filesData = details?.files || [];
@ -298,11 +297,11 @@ export default function FormAudioDetail() {
(file: any) => (file: any) =>
file.contentType && file.contentType &&
(file.contentType.startsWith("audio/") || (file.contentType.startsWith("audio/") ||
file.contentType.includes("webm")) file.contentType.includes("mpeg"))
); );
const fileUrls = audioFiles.map((file: { secondaryUrl: string }) => const fileUrls = audioFiles.map((file: { url: string }) =>
file.secondaryUrl ? file.secondaryUrl : "default-audio.mp3" file.url ? file.url : ""
); );
console.log("Audio file URLs:", fileUrls); console.log("Audio file URLs:", fileUrls);
@ -523,16 +522,23 @@ export default function FormAudioDetail() {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{/* Show the category from details if it doesn't exist in categories list */} {/* Show the category from details if it doesn't exist in categories list */}
{detail && !categories.find(cat => String(cat.id) === String(detail.category.id)) && ( {detail &&
<SelectItem !categories.find(
key={String(detail.category.id)} (cat) =>
value={String(detail.category.id)} String(cat.id) === String(detail.category.id)
> ) && (
{detail.category.name} <SelectItem
</SelectItem> key={String(detail.category.id)}
)} value={String(detail.category.id)}
>
{detail.category.name}
</SelectItem>
)}
{categories.map((category) => ( {categories.map((category) => (
<SelectItem key={String(category.id)} value={String(category.id)}> <SelectItem
key={String(category.id)}
value={String(category.id)}
>
{category.name} {category.name}
</SelectItem> </SelectItem>
))} ))}

View File

@ -211,6 +211,9 @@ export default function FormAudio() {
tags: z tags: z
.array(z.string().min(1)) .array(z.string().min(1))
.min(1, { message: "Wajib isi minimal 1 tag" }), .min(1, { message: "Wajib isi minimal 1 tag" }),
publishedFor: z
.array(z.string())
.min(1, { message: "Minimal 1 target publish harus dipilih." }),
}); });
const { const {
@ -227,6 +230,7 @@ export default function FormAudio() {
rewriteDescription: "", rewriteDescription: "",
category: "", category: "",
tags: [], tags: [],
publishedFor: [],
}, },
}); });
@ -479,10 +483,8 @@ export default function FormAudio() {
const handleCheckboxChange = (id: string): void => { const handleCheckboxChange = (id: string): void => {
if (id === "all") { if (id === "all") {
if (publishedFor.includes("all")) { if (publishedFor.includes("all")) {
// Uncheck all checkboxes
setPublishedFor([]); setPublishedFor([]);
} else { } else {
// Select all checkboxes
setPublishedFor( setPublishedFor(
options options
.filter((opt: any) => opt.id !== "all") .filter((opt: any) => opt.id !== "all")
@ -493,8 +495,6 @@ export default function FormAudio() {
const updatedPublishedFor = publishedFor.includes(id) const updatedPublishedFor = publishedFor.includes(id)
? publishedFor.filter((item) => item !== id) ? publishedFor.filter((item) => item !== id)
: [...publishedFor, id]; : [...publishedFor, id];
// Remove "all" if any checkbox is unchecked
if (publishedFor.includes("all") && id !== "all") { if (publishedFor.includes("all") && id !== "all") {
setPublishedFor(updatedPublishedFor.filter((item) => item !== "all")); setPublishedFor(updatedPublishedFor.filter((item) => item !== "all"));
} else { } else {
@ -505,7 +505,6 @@ export default function FormAudio() {
useEffect(() => { useEffect(() => {
if (articleBody) { if (articleBody) {
// Set ke dua field jika rewrite juga aktif
setValue("description", articleBody); setValue("description", articleBody);
setValue("rewriteDescription", articleBody); setValue("rewriteDescription", articleBody);
} }
@ -513,13 +512,21 @@ export default function FormAudio() {
const save = async (data: AudioSchema) => { const save = async (data: AudioSchema) => {
loading(); loading();
if (files.length === 0) {
MySwal.fire("Error", "Minimal 1 file harus diunggah.", "error");
return;
}
const finalTags = tags.join(", "); const finalTags = tags.join(", ");
const finalTitle = isSwitchOn ? title : data.title; const finalTitle = isSwitchOn ? title : data.title;
// const finalDescription = articleBody || data.description;
const finalDescription = isSwitchOn const finalDescription = isSwitchOn
? data.description ? data.description
: selectedFileType === "rewrite" : selectedFileType === "rewrite"
? data.rewriteDescription ? data.rewriteDescription
: data.descriptionOri; : data.descriptionOri;
if (!finalDescription?.trim()) { if (!finalDescription?.trim()) {
MySwal.fire("Error", "Deskripsi tidak boleh kosong.", "error"); MySwal.fire("Error", "Deskripsi tidak boleh kosong.", "error");
return; return;
@ -567,40 +574,36 @@ export default function FormAudio() {
const response = await createMedia(requestData); const response = await createMedia(requestData);
console.log("Form Data Submitted:", requestData); console.log("Form Data Submitted:", requestData);
if (response?.error) {
MySwal.fire("Error", response?.message, "error");
return;
}
Cookies.set("idCreate", response?.data?.data, { expires: 1 }); Cookies.set("idCreate", response?.data?.data, { expires: 1 });
id = response?.data?.data; id = response?.data?.data;
const formMedia = new FormData(); const formMedia = new FormData();
console.log("Thumbnail : ", files[0]); const thumbnail = files[0];
formMedia.append("file", files[0]); formMedia.append("file", thumbnail);
const responseThumbnail = await uploadThumbnail(id, formMedia); const responseThumbnail = await uploadThumbnail(id, formMedia);
if (responseThumbnail?.error == true) { if (responseThumbnail?.error == true) {
error(responseThumbnail?.message); error(responseThumbnail?.message);
return false; return false;
} }
} }
const progressInfoArr = files.map((item) => ({
const progressInfoArr = []; percentage: 0,
for (const item of files) { fileName: item.name,
progressInfoArr.push({ percentage: 0, fileName: item.name }); }));
}
progressInfo = progressInfoArr; progressInfo = progressInfoArr;
setIsStartUpload(true); setIsStartUpload(true);
setProgressList(progressInfoArr); setProgressList(progressInfoArr);
close(); close();
// showProgress();
files.map(async (item: any, index: number) => { files.map(async (item: any, index: number) => {
await uploadResumableFile(index, String(id), item, "0"); await uploadResumableFile(
index,
String(id),
item,
fileTypeId == "2" || fileTypeId == "4" ? item.duration : "0"
);
}); });
Cookies.remove("idCreate"); Cookies.remove("idCreate");
// MySwal.fire("Sukses", "Data berhasil disimpan.", "success");
}; };
const onSubmit = (data: AudioSchema) => { const onSubmit = (data: AudioSchema) => {
@ -1413,29 +1416,73 @@ export default function FormAudio() {
)} )}
</div> </div>
<div className="px-3 py-3"> <Controller
<div className="flex flex-col gap-3 space-y-2"> control={control}
<Label> name="publishedFor"
{t("publish-target", { defaultValue: "Publish Target" })} render={({ field }) => (
</Label> <div className="px-3 py-3">
{options.map((option) => ( <div className="flex flex-col gap-3 space-y-2">
<div key={option.id} className="flex gap-2 items-center"> <Label>
<Checkbox {t("publish-target", { defaultValue: "Publish Target" })}
id={option.id} </Label>
checked={
{options.map((option) => {
const isAllChecked =
field.value.length ===
options.filter((opt: any) => opt.id !== "all").length;
const isChecked =
option.id === "all" option.id === "all"
? publishedFor.length === ? isAllChecked
options.filter((opt: any) => opt.id !== "all") : field.value.includes(option.id);
.length
: publishedFor.includes(option.id) const handleChange = () => {
} let updated: string[] = [];
onCheckedChange={() => handleCheckboxChange(option.id)}
/> if (option.id === "all") {
<Label htmlFor={option.id}>{option.label}</Label> updated = isAllChecked
? []
: options
.filter((opt: any) => opt.id !== "all")
.map((opt: any) => opt.id);
} else {
updated = isChecked
? field.value.filter((val) => val !== option.id)
: [...field.value, option.id];
if (isAllChecked && option.id !== "all") {
updated = updated.filter((val) => val !== "all");
}
}
field.onChange(updated);
setPublishedFor(updated);
};
return (
<div
key={option.id}
className="flex gap-2 items-center"
>
<Checkbox
id={option.id}
checked={isChecked}
onCheckedChange={handleChange}
/>
<Label htmlFor={option.id}>{option.label}</Label>
</div>
);
})}
{errors.publishedFor && (
<p className="text-red-500 text-sm">
{errors.publishedFor.message}
</p>
)}
</div> </div>
))} </div>
</div> )}
</div> />
</Card> </Card>
<div className="flex flex-row justify-end gap-3"> <div className="flex flex-row justify-end gap-3">
<div className="mt-4"> <div className="mt-4">

View File

@ -59,6 +59,7 @@ import { getCsrfToken } from "@/service/auth";
import { Link } from "@/i18n/routing"; import { Link } from "@/i18n/routing";
import { request } from "http"; import { request } from "http";
import { useLocale, useTranslations } from "next-intl"; import { useLocale, useTranslations } from "next-intl";
import { toast } from "sonner";
interface FileWithPreview extends File { interface FileWithPreview extends File {
preview: string; preview: string;
@ -153,27 +154,77 @@ export default function FormImage() {
{ id: "8", label: "KSP" }, { id: "8", label: "KSP" },
]; ];
type FileWithPreview = File & {
preview: string;
};
const MAX_FILE_SIZE = 100 * 1024 * 1024;
const { getRootProps, getInputProps } = useDropzone({ const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) => {
setFiles(acceptedFiles.map((file) => Object.assign(file)));
},
accept: { accept: {
"image/*": [], "image/jpeg": [],
"image/png": [],
"image/jpg": [],
},
onDrop: (acceptedFiles) => {
const validFiles = acceptedFiles
.filter(
(file) =>
["image/jpeg", "image/png", "image/jpg"].includes(file.type) &&
file.size <= MAX_FILE_SIZE
)
.map((file) =>
Object.assign(file, {
preview: URL.createObjectURL(file),
})
);
if (validFiles.length === 0) {
toast.error(
"File tidak valid. Hanya .jpg, .jpeg, .png maksimal 100MB yang diperbolehkan."
);
return;
}
setFiles(validFiles);
setValue("files", validFiles);
}, },
}); });
useEffect(() => {
return () => {
files.forEach((file) => URL.revokeObjectURL(file.preview));
};
}, [files]);
const imageSchema = z.object({ const imageSchema = z.object({
title: z.string().min(1, { message: t("titleRequired") }), title: z.string().min(1, { message: t("titleRequired") }),
description: z.string().optional(), description: z.string().optional(),
descriptionOri: z.string().optional(), descriptionOri: z.string().optional(),
rewriteDescription: z.string().optional(), rewriteDescription: z.string().optional(),
creatorName: z.string().min(1, { message: t("creatorRequired") }), creatorName: z.string().min(1, { message: t("creatorRequired") }),
categoryId: z.string().min(1, { message: "Kategori diperlukan" }), files: z
tags: z.array(z.string()).min(1, { message: "Minimal 1 tag diperlukan" }), .array(z.any())
.min(1, { message: "Minimal 1 file harus diunggah." })
.refine(
(files) =>
files.every(
(file: File) =>
["image/jpeg", "image/png", "image/jpg"].includes(file.type) &&
file.size <= 100 * 1024 * 1024
),
{
message:
"Hanya file .jpg, .jpeg, .png, maksimal 100MB yang diperbolehkan.",
}
),
categoryId: z.string().min(1, { message: "Kategori wajib dipilih." }),
tags: z
.array(z.string())
.min(1, { message: "Minimal 1 tag harus ditambahkan." }),
publishedFor: z publishedFor: z
.array(z.string()) .array(z.string())
.min(1, { message: "Pilih target publish" }), .min(1, { message: "Minimal 1 target publish harus dipilih." }),
files: z.array(z.any()).min(1, { message: "File harus diupload" }),
}); });
const { const {
@ -186,15 +237,15 @@ export default function FormImage() {
} = useForm<ImageSchema>({ } = useForm<ImageSchema>({
resolver: zodResolver(imageSchema), resolver: zodResolver(imageSchema),
defaultValues: { defaultValues: {
title: "",
description: "", description: "",
descriptionOri: "", descriptionOri: "",
rewriteDescription: "", rewriteDescription: "",
title: "",
creatorName: "", creatorName: "",
files: [],
categoryId: "", categoryId: "",
tags: [], tags: [],
publishedFor: [], publishedFor: [],
files: [],
}, },
}); });
@ -395,7 +446,9 @@ export default function FormImage() {
e.preventDefault(); e.preventDefault();
const newTag = e.currentTarget.value.trim(); const newTag = e.currentTarget.value.trim();
if (!tags.includes(newTag)) { if (!tags.includes(newTag)) {
setTags((prevTags) => [...prevTags, newTag]); const updatedTags = [...tags, newTag];
setTags(updatedTags);
setValue("tags", updatedTags);
if (inputRef.current) { if (inputRef.current) {
inputRef.current.value = ""; inputRef.current.value = "";
} }
@ -404,20 +457,15 @@ export default function FormImage() {
}; };
const handleRemoveTag = (index: number) => { const handleRemoveTag = (index: number) => {
setTags((prevTags) => prevTags.filter((_, i) => i !== index)); const updatedTags = tags.filter((_, i) => i !== index);
}; setTags(updatedTags);
setValue("tags", updatedTags);
const handleRemoveImage = (index: number) => {
setSelectedFiles((prevImages) => prevImages.filter((_, i) => i !== index));
}; };
useEffect(() => { useEffect(() => {
async function initState() { async function initState() {
getCategories(); getCategories();
// setVideoActive(fileTypeId == '2');
// getRoles();
} }
initState(); initState();
}, []); }, []);
@ -478,6 +526,12 @@ export default function FormImage() {
const save = async (data: ImageSchema) => { const save = async (data: ImageSchema) => {
loading(); loading();
if (files.length === 0) {
MySwal.fire("Error", "Minimal 1 file harus diunggah.", "error");
return;
}
const finalTags = tags.join(", "); const finalTags = tags.join(", ");
const finalTitle = isSwitchOn ? title : data.title; const finalTitle = isSwitchOn ? title : data.title;
// const finalDescription = articleBody || data.description; // const finalDescription = articleBody || data.description;
@ -817,10 +871,10 @@ export default function FormImage() {
)} )}
</div> </div>
<div className="flex items-center"> {/* <div className="flex items-center">
<div className="py-3 space-y-2 w-full"> <div className="py-3 space-y-2 w-full">
<Label>{t("category", { defaultValue: "Category" })}</Label> <Label>{t("category", { defaultValue: "Category" })}</Label>
{/* <Select <Select
value={selectedCategory} value={selectedCategory}
onValueChange={(id) => { onValueChange={(id) => {
console.log("Selected Category ID:", id); console.log("Selected Category ID:", id);
@ -840,35 +894,46 @@ export default function FormImage() {
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> */} </Select>
<Controller
control={control}
name="categoryId"
render={({ field }) => (
<Select
value={field.value}
onValueChange={field.onChange}
>
<SelectTrigger>
<SelectValue placeholder="Pilih" />
</SelectTrigger>
<SelectContent>
{categories.map((cat) => (
<SelectItem key={cat.id} value={cat.id.toString()}>
{cat.name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
{errors.categoryId?.message && (
<p className="text-red-400 text-sm">
{errors.categoryId?.message}
</p>
)}
</div> </div>
</div> </div> */}
<Controller
control={control}
name="categoryId"
render={({ field }) => (
<div className="w-full">
<Label>{t("category", { defaultValue: "Category" })}</Label>
<Select
value={field.value}
onValueChange={(value) => {
field.onChange(value);
setSelectedCategory(value);
}}
>
<SelectTrigger size="md">
<SelectValue placeholder="Pilih" />
</SelectTrigger>
<SelectContent>
{categories.map((category) => (
<SelectItem
key={category.id}
value={category.id.toString()}
>
{category.name}
</SelectItem>
))}
</SelectContent>
</Select>
{errors.categoryId && (
<p className="text-sm text-red-500 mt-1">
{errors.categoryId.message}
</p>
)}
</div>
)}
/>
<div className="flex flex-row items-center gap-3 py-3 "> <div className="flex flex-row items-center gap-3 py-3 ">
<Label> <Label>
{t("ai-assistance", { defaultValue: "Ai Assistance" })} {t("ai-assistance", { defaultValue: "Ai Assistance" })}
@ -1232,212 +1297,46 @@ export default function FormImage() {
</RadioGroup> </RadioGroup>
</> </>
)} )}
<div className="py-3 space-y-2">
<Label>
{t("select-file", { defaultValue: "Select File" })}
</Label>
<div> <div {...getRootProps({ className: "dropzone" })}>
<Controller <input {...getInputProps()} />
control={control} <div className=" w-full text-center border-dashed border border-default-200 dark:border-default-300 rounded-md py-[52px] flex items-center flex-col">
name="files" <CloudUpload className="text-default-300 w-10 h-10" />
render={({ field }) => { <h4 className=" text-2xl font-medium mb-1 mt-3 text-card-foreground/80">
const maxSize = 100 * 1024 * 1024; {t("drag-file", { defaultValue: "Drag File" })}
const [previews, setPreviews] = useState<string[]>([]); </h4>
<div className=" text-xs text-muted-foreground">
const { getRootProps, getInputProps, fileRejections } = {t("upload-file-max", {
useDropzone({ defaultValue: "Upload File Max",
accept: { })}
"image/jpeg": [".jpeg", ".jpg"],
"image/png": [".png"],
},
maxSize,
multiple: true, // <- Set true jika ingin lebih dari satu
onDrop: (acceptedFiles) => {
const currentFiles = [
...(field.value || []),
...acceptedFiles,
];
field.onChange(currentFiles);
const newPreviews = acceptedFiles.map((file) =>
URL.createObjectURL(file)
);
setPreviews((prev) => [...prev, ...newPreviews]);
},
});
// Clean up URL on unmount
useEffect(() => {
return () => {
previews.forEach((url) => URL.revokeObjectURL(url));
};
}, [previews]);
const handleRemoveFile = (indexToRemove: number) => {
const updatedFiles = [...field.value];
updatedFiles.splice(indexToRemove, 1);
const updatedPreviews = [...previews];
URL.revokeObjectURL(updatedPreviews[indexToRemove]); // Clean up
updatedPreviews.splice(indexToRemove, 1);
field.onChange(updatedFiles);
setPreviews(updatedPreviews);
};
return (
<div className="py-3 space-y-2">
<Label>
{t("select-file", { defaultValue: "Select File" })}
</Label>
<div {...getRootProps({ className: "dropzone" })}>
<input {...getInputProps()} />
<div className="w-full text-center border-dashed border border-default-200 dark:border-default-300 rounded-md py-[52px] flex items-center flex-col">
<CloudUpload className="text-default-300 w-10 h-10" />
<h4 className="text-2xl font-medium mb-1 mt-3 text-card-foreground/80">
{t("drag-file", { defaultValue: "Drag File" })}
</h4>
<div className="text-xs text-muted-foreground">
{t("upload-file-max", {
defaultValue:
"Upload file max 100MB (.jpg, .jpeg, .png)",
})}
</div>
</div>
</div>
{field.value && field.value.length > 0 && (
<div className="mt-3 space-y-3">
{field.value.map((file: File, idx: number) => (
<div
key={idx}
className="flex items-center gap-4 border p-2 rounded-md relative"
>
<img
src={previews[idx]}
alt={`preview-${idx}`}
className="w-24 h-24 object-cover rounded border"
/>
<div className="flex-1 text-sm">
<div className="font-medium">{file.name}</div>
<div className="text-muted-foreground text-xs">
{(file.size / (1024 * 1024)).toFixed(2)} MB
</div>
</div>
<button
type="button"
className="absolute top-1 right-1 text-muted-foreground hover:text-red-500"
onClick={() => handleRemoveFile(idx)}
>
<X className="w-5 h-5" />
</button>
</div>
))}
<div className="flex justify-end">
<Button
type="button"
variant="default"
onClick={() => {
field.onChange([]);
previews.forEach((url) =>
URL.revokeObjectURL(url)
);
setPreviews([]);
}}
>
{t("remove-all", {
defaultValue: "Remove All",
})}
</Button>
</div>
</div>
)}
{errors.files?.message && (
<p className="text-red-400 text-sm">
{errors.files.message}
</p>
)}
{fileRejections.length > 0 && (
<div className="text-red-400 text-sm space-y-1 mt-2">
{fileRejections.map(({ file, errors }, index) => (
<div key={index}>
<p>{file.name}:</p>
<ul className="ml-4 list-disc">
{errors.map((e, idx) => (
<li key={idx}>
{e.code === "file-too-large" &&
t("size", {
defaultValue:
"File too large. Max 100MB.",
})}
{e.code === "file-invalid-type" &&
t("only", {
defaultValue:
"Invalid file type. Only .jpg, .jpeg, .png allowed.",
})}
</li>
))}
</ul>
</div>
))}
</div>
)}
</div>
);
}}
/>
</div>
{/* <Controller
name="files"
control={control}
rules={{
required: t("file-required", {
defaultValue: "File is required",
}),
}}
render={({ field }) => (
<div className="py-3 space-y-2">
<Label>
{t("select-file", { defaultValue: "Select File" })}
</Label>
<div {...getRootProps({ className: "dropzone" })}>
<input {...getInputProps()} />
<div className=" w-full text-center border-dashed border border-default-200 dark:border-default-300 rounded-md py-[52px] flex items-center flex-col">
<CloudUpload className="text-default-300 w-10 h-10" />
<h4 className=" text-2xl font-medium mb-1 mt-3 text-card-foreground/80">
{t("drag-file", { defaultValue: "Drag File" })}
</h4>
<div className=" text-xs text-muted-foreground">
{t("upload-file-max", {
defaultValue: "Upload File Max",
})}
</div>
</div>
</div> </div>
{files.length ? (
<>
<div>{fileList}</div>
<div className="flex justify-between gap-2">
<Button
color="destructive"
onClick={handleRemoveAllFiles}
>
{t("remove-all", { defaultValue: "Remove All" })}
</Button>
</div>
</>
) : null}
{errors.files?.message && (
<p className="text-red-400 text-sm">
{errors.files.message}
</p>
)}
</div> </div>
</div>
{files.length ? (
<>
<div>{fileList}</div>
<div className="flex justify-between gap-2">
<Button
color="destructive"
onClick={handleRemoveAllFiles}
>
{t("remove-all", { defaultValue: "Remove All" })}
</Button>
</div>
</>
) : null}
{errors.files && (
<p className="text-red-500 text-sm mt-1">
{errors.files.message}
</p>
)} )}
/> */} </div>
</div> </div>
</div> </div>
</Card> </Card>
@ -1467,10 +1366,12 @@ export default function FormImage() {
)} )}
</div> </div>
</div> </div>
{/* <div className="px-3 py-3 space-y-2"> {/* <div className="px-3 py-3 space-y-2">
<Label htmlFor="tags"> <Label htmlFor="tags">
{t("tags", { defaultValue: "Tags" })} {t("tags", { defaultValue: "Tags" })}
</Label> </Label>
<Input <Input
type="text" type="text"
id="tags" id="tags"
@ -1501,57 +1402,37 @@ export default function FormImage() {
{t("tags", { defaultValue: "Tags" })} {t("tags", { defaultValue: "Tags" })}
</Label> </Label>
<Controller <Input
control={control} type="text"
name="tags" id="tags"
render={({ field }) => ( placeholder="Add a tag and press Enter"
<> onKeyDown={handleAddTag}
<Input ref={inputRef}
type="text"
id="tags"
placeholder="Add a tag and press Enter"
onKeyDown={(e) => {
if (e.key === "Enter" && e.currentTarget.value.trim()) {
e.preventDefault();
field.onChange([
...field.value,
e.currentTarget.value.trim(),
]);
e.currentTarget.value = "";
}
}}
/>
<div className="mt-3">
{field.value.map((tag: string, index: number) => (
<span
key={index}
className="px-1 py-1 rounded-lg bg-black text-white mr-2 text-sm font-sans"
>
{tag}{" "}
<button
type="button"
onClick={() => {
const updatedTags = field.value.filter(
(_, i) => i !== index
);
field.onChange(updatedTags);
}}
className="remove-tag-button"
>
×
</button>
</span>
))}
</div>
</>
)}
/> />
{/* Tampilkan error */} {errors.tags && (
{errors.tags?.message && ( <p className="text-sm text-red-500 mt-1">
<p className="text-red-400 text-sm">{errors.tags.message}</p> {errors.tags.message}
</p>
)} )}
<div className="mt-3">
{tags.map((tag, index) => (
<span
key={index}
className="px-1 py-1 rounded-lg bg-black text-white mr-2 text-sm font-sans"
>
{tag}{" "}
<button
type="button"
onClick={() => handleRemoveTag(index)}
className="remove-tag-button"
>
×
</button>
</span>
))}
</div>
</div> </div>
{/* <div className="px-3 py-3"> {/* <div className="px-3 py-3">
@ -1577,63 +1458,76 @@ export default function FormImage() {
))} ))}
</div> </div>
</div> */} </div> */}
<div className="px-3 py-3"> <Controller
<div className="flex flex-col gap-3 space-y-2"> control={control}
<Label> name="publishedFor"
{t("publish-target", { defaultValue: "Publish Target" })} render={({ field }) => (
</Label> <div className="px-3 py-3">
<div className="flex flex-col gap-3 space-y-2">
<Label>
{t("publish-target", { defaultValue: "Publish Target" })}
</Label>
<Controller {options.map((option) => {
control={control} const isAllChecked =
name="publishedFor" field.value.length ===
render={({ field }) => ( options.filter((opt: any) => opt.id !== "all").length;
<>
{options.map((option) => ( const isChecked =
option.id === "all"
? isAllChecked
: field.value.includes(option.id);
const handleChange = () => {
let updated: string[] = [];
if (option.id === "all") {
updated = isAllChecked
? []
: options
.filter((opt: any) => opt.id !== "all")
.map((opt: any) => opt.id);
} else {
updated = isChecked
? field.value.filter((val) => val !== option.id)
: [...field.value, option.id];
if (isAllChecked && option.id !== "all") {
updated = updated.filter((val) => val !== "all");
}
}
field.onChange(updated);
setPublishedFor(updated);
};
return (
<div <div
key={option.id} key={option.id}
className="flex gap-2 items-center" className="flex gap-2 items-center"
> >
<Checkbox <Checkbox
id={option.id} id={option.id}
checked={field.value.includes(option.id)} checked={isChecked}
onCheckedChange={(checked: boolean) => { onCheckedChange={handleChange}
let updated: string[] = [...field.value];
if (checked) {
updated.push(option.id);
} else {
updated = updated.filter(
(id) => id !== option.id
);
}
if (option.id === "all") {
if (checked) {
updated = options
.filter((opt) => opt.id !== "all")
.map((opt) => opt.id);
} else {
updated = [];
}
}
field.onChange(updated);
}}
/> />
<Label htmlFor={option.id}>{option.label}</Label> <Label htmlFor={option.id}>{option.label}</Label>
</div> </div>
))} );
})}
{/* Tampilkan error */} {errors.publishedFor && (
{errors.publishedFor?.message && ( <p className="text-red-500 text-sm">
<p className="text-red-400 text-sm"> {errors.publishedFor.message}
{errors.publishedFor.message} </p>
</p> )}
)} </div>
</> </div>
)} )}
/> />
</div>
</div>
</Card> </Card>
{/* button submit */}
<div className="flex flex-row justify-end gap-3"> <div className="flex flex-row justify-end gap-3">
<div className="mt-4"> <div className="mt-4">
<Button type="submit" color="primary"> <Button type="submit" color="primary">

View File

@ -83,24 +83,20 @@ export default function FormTeks() {
const router = useRouter(); const router = useRouter();
const editor = useRef(null); const editor = useRef(null);
type TeksSchema = z.infer<typeof teksSchema>; type TeksSchema = z.infer<typeof teksSchema>;
const params = useParams(); const params = useParams();
const locale = params?.locale; const locale = params?.locale;
const [selectedFiles, setSelectedFiles] = useState<File[]>([]); const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const taskId = Cookies.get("taskId"); const taskId = Cookies.get("taskId");
const scheduleId = Cookies.get("scheduleId"); const scheduleId = Cookies.get("scheduleId");
const scheduleType = Cookies.get("scheduleType"); const scheduleType = Cookies.get("scheduleType");
const roleId = getCookiesDecrypt("urie"); const roleId = getCookiesDecrypt("urie");
const [selectedFileType, setSelectedFileType] = useState("original"); const [selectedFileType, setSelectedFileType] = useState("original");
const [categories, setCategories] = useState<Category[]>([]); const [categories, setCategories] = useState<Category[]>([]);
const [selectedCategory, setSelectedCategory] = useState<any>(); const [selectedCategory, setSelectedCategory] = useState<any>();
const [tags, setTags] = useState<any[]>([]); const [tags, setTags] = useState<any[]>([]);
const [thumbnail, setThumbnail] = useState<File | null>(null); const [thumbnail, setThumbnail] = useState<File | null>(null);
const [preview, setPreview] = useState<string | null>(null); const [preview, setPreview] = useState<string | null>(null);
const [selectedLanguage, setSelectedLanguage] = useState(""); const [selectedLanguage, setSelectedLanguage] = useState("");
const [selectedSEO, setSelectedSEO] = useState<string>(""); const [selectedSEO, setSelectedSEO] = useState<string>("");
const [title, setTitle] = useState<string>(""); const [title, setTitle] = useState<string>("");
const [selectedAdvConfig, setSelectedAdvConfig] = useState<string>(""); const [selectedAdvConfig, setSelectedAdvConfig] = useState<string>("");
@ -109,9 +105,8 @@ export default function FormTeks() {
const [isLoadingData, setIsLoadingData] = useState<boolean>(false); const [isLoadingData, setIsLoadingData] = useState<boolean>(false);
const [selectedWritingStyle, setSelectedWritingStyle] = const [selectedWritingStyle, setSelectedWritingStyle] =
useState("professional"); useState("professional");
const [editorContent, setEditorContent] = useState(""); // Untuk original editor const [editorContent, setEditorContent] = useState("");
const [rewriteEditorContent, setRewriteEditorContent] = useState(""); const [rewriteEditorContent, setRewriteEditorContent] = useState("");
const [articleIds, setArticleIds] = useState<string[]>([]); const [articleIds, setArticleIds] = useState<string[]>([]);
const [isGeneratedArticle, setIsGeneratedArticle] = useState(false); const [isGeneratedArticle, setIsGeneratedArticle] = useState(false);
const [articleBody, setArticleBody] = useState<string>(""); const [articleBody, setArticleBody] = useState<string>("");
@ -192,8 +187,8 @@ export default function FormTeks() {
creatorName: z.string().min(1, { message: "Creator diperlukan" }), creatorName: z.string().min(1, { message: "Creator diperlukan" }),
category: z.string().min(1, { message: "Kategori harus dipilih" }), category: z.string().min(1, { message: "Kategori harus dipilih" }),
tags: z tags: z
.array(z.string().min(1)) .array(z.string())
.min(1, { message: "Minimal 1 tag diperlukan" }), .min(1, { message: "Minimal 1 tag harus ditambahkan." }),
files: z files: z
.array( .array(
z z
@ -220,6 +215,9 @@ export default function FormTeks() {
description: z.string().optional(), description: z.string().optional(),
descriptionOri: z.string().optional(), descriptionOri: z.string().optional(),
rewriteDescription: z.string().optional(), rewriteDescription: z.string().optional(),
publishedFor: z
.array(z.string())
.min(1, { message: "Minimal 1 target publish harus dipilih." }),
}); });
const { const {
@ -228,6 +226,7 @@ export default function FormTeks() {
getValues, getValues,
setValue, setValue,
formState: { errors }, formState: { errors },
trigger,
} = useForm<TeksSchema>({ } = useForm<TeksSchema>({
resolver: zodResolver(teksSchema), resolver: zodResolver(teksSchema),
defaultValues: { defaultValues: {
@ -239,9 +238,14 @@ export default function FormTeks() {
description: "", description: "",
descriptionOri: "", descriptionOri: "",
rewriteDescription: "", rewriteDescription: "",
publishedFor: [],
}, },
}); });
useEffect(() => {
setValue("publishedFor", publishedFor);
}, [publishedFor, setValue]);
const doGenerateMainKeyword = async () => { const doGenerateMainKeyword = async () => {
console.log(selectedMainKeyword); console.log(selectedMainKeyword);
if (selectedMainKeyword?.length > 1) { if (selectedMainKeyword?.length > 1) {
@ -434,22 +438,25 @@ export default function FormTeks() {
} }
}; };
const handleAddTag = ( const handleAddTag = (e: React.KeyboardEvent<HTMLInputElement>) => {
e: React.KeyboardEvent<HTMLInputElement>, if (e.key === "Enter" && e.currentTarget.value.trim()) {
field: any
) => {
if (e.key === "Enter") {
e.preventDefault(); e.preventDefault();
const value = e.currentTarget.value.trim(); const newTag = e.currentTarget.value.trim();
if (value && !field.value.includes(value)) { if (!tags.includes(newTag)) {
field.onChange([...field.value, value]); const updatedTags = [...tags, newTag];
e.currentTarget.value = ""; setTags(updatedTags);
setValue("tags", updatedTags);
if (inputRef.current) {
inputRef.current.value = "";
}
} }
} }
}; };
const handleRemoveTag = (index: number, field: any) => {
const newTags = field.value.filter((_: any, i: number) => i !== index); const handleRemoveTag = (index: number) => {
field.onChange(newTags); const updatedTags = tags.filter((_, i) => i !== index);
setTags(updatedTags);
setValue("tags", updatedTags);
}; };
const handleRemoveImage = (index: number) => { const handleRemoveImage = (index: number) => {
@ -528,9 +535,15 @@ export default function FormTeks() {
const save = async (data: TeksSchema) => { const save = async (data: TeksSchema) => {
loading(); loading();
if (files.length === 0) {
MySwal.fire("Error", "Minimal 1 file harus diunggah.", "error");
return;
}
const finalTags = tags.join(", "); const finalTags = tags.join(", ");
const finalTitle = isSwitchOn ? title : data.title; const finalTitle = isSwitchOn ? title : data.title;
// const finalDescription = articleBody || data.description;
const finalDescription = isSwitchOn const finalDescription = isSwitchOn
? data.description ? data.description
: selectedFileType === "rewrite" : selectedFileType === "rewrite"
@ -541,6 +554,7 @@ export default function FormTeks() {
MySwal.fire("Error", "Deskripsi tidak boleh kosong.", "error"); MySwal.fire("Error", "Deskripsi tidak boleh kosong.", "error");
return; return;
} }
let requestData: { let requestData: {
title: string; title: string;
description: string; description: string;
@ -555,7 +569,7 @@ export default function FormTeks() {
tags: string; tags: string;
isYoutube: boolean; isYoutube: boolean;
isInternationalMedia: boolean; isInternationalMedia: boolean;
attachFromScheduleId?: number; // ✅ Tambahkan properti ini attachFromScheduleId?: number;
} = { } = {
...data, ...data,
title: finalTitle, title: finalTitle,
@ -576,49 +590,43 @@ export default function FormTeks() {
let id = Cookies.get("idCreate"); let id = Cookies.get("idCreate");
if (scheduleId !== undefined) { if (scheduleId !== undefined) {
requestData.attachFromScheduleId = Number(scheduleId); // ✅ Tambahkan nilai ini requestData.attachFromScheduleId = Number(scheduleId);
} }
if (id == undefined) { if (id == undefined) {
const response = await createMedia(requestData); const response = await createMedia(requestData);
console.log("Form Data Submitted:", requestData); console.log("Form Data Submitted:", requestData);
if (response?.error) {
MySwal.fire("Error", response?.message, "error");
return;
}
Cookies.set("idCreate", response?.data?.data, { expires: 1 }); Cookies.set("idCreate", response?.data?.data, { expires: 1 });
id = response?.data?.data; id = response?.data?.data;
// Upload Thumbnail
const formMedia = new FormData(); const formMedia = new FormData();
console.log("Thumbnail : ", files[0]); const thumbnail = files[0];
formMedia.append("file", files[0]); formMedia.append("file", thumbnail);
const responseThumbnail = await uploadThumbnail(id, formMedia); const responseThumbnail = await uploadThumbnail(id, formMedia);
if (responseThumbnail?.error == true) { if (responseThumbnail?.error == true) {
error(responseThumbnail?.message); error(responseThumbnail?.message);
return false; return false;
} }
} }
const progressInfoArr = files.map((item) => ({
// Upload File percentage: 0,
const progressInfoArr = []; fileName: item.name,
for (const item of files) { }));
progressInfoArr.push({ percentage: 0, fileName: item.name });
}
progressInfo = progressInfoArr; progressInfo = progressInfoArr;
setIsStartUpload(true); setIsStartUpload(true);
setProgressList(progressInfoArr); setProgressList(progressInfoArr);
close(); close();
// showProgress();
files.map(async (item: any, index: number) => { files.map(async (item: any, index: number) => {
await uploadResumableFile(index, String(id), item, "0"); await uploadResumableFile(
index,
String(id),
item,
fileTypeId == "2" || fileTypeId == "4" ? item.duration : "0"
);
}); });
Cookies.remove("idCreate"); Cookies.remove("idCreate");
// MySwal.fire("Sukses", "Data berhasil disimpan.", "success");
}; };
const onSubmit = (data: TeksSchema) => { const onSubmit = (data: TeksSchema) => {
@ -796,7 +804,6 @@ export default function FormTeks() {
}; };
useEffect(() => { useEffect(() => {
// Jika input title kosong, isi dengan hasil generate title
if (!getValues("title") && title) { if (!getValues("title") && title) {
setValue("title", title); setValue("title", title);
} }
@ -810,7 +817,7 @@ export default function FormTeks() {
lang: "id", lang: "id",
contextType: "text", contextType: "text",
urlContext: null, urlContext: null,
context: editorContent, // Ambil isi editor original context: editorContent,
createdBy: roleId, createdBy: roleId,
sentiment: "Humorous", sentiment: "Humorous",
clientId: "7QTW8cMojyayt6qnhqTOeJaBI70W4EaQ", clientId: "7QTW8cMojyayt6qnhqTOeJaBI70W4EaQ",
@ -1353,44 +1360,41 @@ export default function FormTeks() {
<Label htmlFor="tags"> <Label htmlFor="tags">
{t("tags", { defaultValue: "Tags" })} {t("tags", { defaultValue: "Tags" })}
</Label> </Label>
<Controller
control={control} <Input
name="tags" type="text"
render={({ field }) => ( id="tags"
<> placeholder="Add a tag and press Enter"
<Input onKeyDown={handleAddTag}
type="text" ref={inputRef}
id="tags"
placeholder="Add a tag and press Enter"
onKeyDown={(e) => handleAddTag(e, field)} // pass field ke fungsi
ref={inputRef}
/>
<div className="mt-3">
{field.value.map((tag, index) => (
<span
key={index}
className="px-1 py-1 rounded-lg bg-black text-white mr-2 text-sm font-sans"
>
{tag}{" "}
<button
type="button"
onClick={() => handleRemoveTag(index, field)}
className="remove-tag-button"
>
×
</button>
</span>
))}
</div>
</>
)}
/> />
{errors.tags?.message && (
<p className="text-red-400 text-sm">{errors.tags.message}</p> {errors.tags && (
<p className="text-sm text-red-500 mt-1">
{errors.tags.message}
</p>
)} )}
<div className="mt-3">
{tags.map((tag, index) => (
<span
key={index}
className="px-1 py-1 rounded-lg bg-black text-white mr-2 text-sm font-sans"
>
{tag}{" "}
<button
type="button"
onClick={() => handleRemoveTag(index)}
className="remove-tag-button"
>
×
</button>
</span>
))}
</div>
</div> </div>
<div className="px-3 py-3"> {/* <div className="px-3 py-3">
<div className="flex flex-col gap-3 space-y-2"> <div className="flex flex-col gap-3 space-y-2">
<Label> <Label>
{t("publish-target", { defaultValue: "Publish Target" })} {t("publish-target", { defaultValue: "Publish Target" })}
@ -1412,7 +1416,74 @@ export default function FormTeks() {
</div> </div>
))} ))}
</div> </div>
</div> </div> */}
<Controller
control={control}
name="publishedFor"
render={({ field }) => (
<div className="px-3 py-3">
<div className="flex flex-col gap-3 space-y-2">
<Label>
{t("publish-target", { defaultValue: "Publish Target" })}
</Label>
{options.map((option) => {
const isAllChecked =
field.value.length ===
options.filter((opt: any) => opt.id !== "all").length;
const isChecked =
option.id === "all"
? isAllChecked
: field.value.includes(option.id);
const handleChange = () => {
let updated: string[] = [];
if (option.id === "all") {
updated = isAllChecked
? []
: options
.filter((opt: any) => opt.id !== "all")
.map((opt: any) => opt.id);
} else {
updated = isChecked
? field.value.filter((val) => val !== option.id)
: [...field.value, option.id];
if (isAllChecked && option.id !== "all") {
updated = updated.filter((val) => val !== "all");
}
}
field.onChange(updated);
setPublishedFor(updated);
};
return (
<div
key={option.id}
className="flex gap-2 items-center"
>
<Checkbox
id={option.id}
checked={isChecked}
onCheckedChange={handleChange}
/>
<Label htmlFor={option.id}>{option.label}</Label>
</div>
);
})}
{errors.publishedFor && (
<p className="text-red-500 text-sm">
{errors.publishedFor.message}
</p>
)}
</div>
</div>
)}
/>
</Card> </Card>
<div className="flex flex-row justify-end gap-3"> <div className="flex flex-row justify-end gap-3">
<div className="mt-4"> <div className="mt-4">

View File

@ -85,7 +85,6 @@ export default function FormVideo() {
type VideoSchema = z.infer<typeof videoSchema>; type VideoSchema = z.infer<typeof videoSchema>;
const params = useParams(); const params = useParams();
const locale = params?.locale; const locale = params?.locale;
const [selectedFiles, setSelectedFiles] = useState<File[]>([]); const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const taskId = Cookies.get("taskId"); const taskId = Cookies.get("taskId");
const scheduleId = Cookies.get("scheduleId"); const scheduleId = Cookies.get("scheduleId");
@ -116,7 +115,9 @@ export default function FormVideo() {
null null
); );
const [selectedMainKeyword, setSelectedMainKeyword] = useState(""); const [selectedMainKeyword, setSelectedMainKeyword] = useState("");
// const [selectedWritingStyle, setSelectedWritingStyle] = useState(""); const [publishedForError, setPublishedForError] = useState<string | null>(
null
);
const [selectedSize, setSelectedSize] = useState(""); const [selectedSize, setSelectedSize] = useState("");
const [detailData, setDetailData] = useState<any>(null); const [detailData, setDetailData] = useState<any>(null);
const [articleImages, setArticleImages] = useState<string[]>([]); const [articleImages, setArticleImages] = useState<string[]>([]);
@ -211,6 +212,9 @@ export default function FormVideo() {
(files) => files.every((file: File) => file.size <= MAX_FILE_SIZE), (files) => files.every((file: File) => file.size <= MAX_FILE_SIZE),
{ message: "Ukuran file maksimal 100 MB" } { message: "Ukuran file maksimal 100 MB" }
), ),
publishedFor: z
.array(z.string())
.min(1, { message: "Minimal 1 target publish harus dipilih." }),
}); });
const { const {
@ -228,9 +232,14 @@ export default function FormVideo() {
category: "", category: "",
files: [], files: [],
tags: [], tags: [],
publishedFor: [],
}, },
}); });
useEffect(() => {
setValue("publishedFor", publishedFor);
}, [publishedFor, setValue]);
const doGenerateMainKeyword = async () => { const doGenerateMainKeyword = async () => {
console.log(selectedMainKeyword); console.log(selectedMainKeyword);
if (selectedMainKeyword?.length > 1) { if (selectedMainKeyword?.length > 1) {
@ -511,6 +520,12 @@ export default function FormVideo() {
}, [articleBody, setValue]); }, [articleBody, setValue]);
const save = async (data: VideoSchema) => { const save = async (data: VideoSchema) => {
if (publishedFor.length === 0) {
setPublishedForError("Minimal 1 target publish harus dipilih.");
return;
} else {
setPublishedForError(null);
}
loading(); loading();
const finalTags = data.tags.join(", "); const finalTags = data.tags.join(", ");
const finalTitle = isSwitchOn ? title : data.title; const finalTitle = isSwitchOn ? title : data.title;
@ -600,8 +615,6 @@ export default function FormVideo() {
}); });
Cookies.remove("idCreate"); Cookies.remove("idCreate");
// MySwal.fire("Sukses", "Data berhasil disimpan.", "success");
}; };
useEffect(() => { useEffect(() => {
@ -1427,29 +1440,73 @@ export default function FormVideo() {
<p className="text-red-400 text-sm">{errors.tags.message}</p> <p className="text-red-400 text-sm">{errors.tags.message}</p>
)} )}
</div> </div>
<div className="px-3 py-3"> <Controller
<div className="flex flex-col gap-3 space-y-2"> control={control}
<Label> name="publishedFor"
{t("publish-target", { defaultValue: "Publish Target" })} render={({ field }) => (
</Label> <div className="px-3 py-3">
{options.map((option) => ( <div className="flex flex-col gap-3 space-y-2">
<div key={option.id} className="flex gap-2 items-center"> <Label>
<Checkbox {t("publish-target", { defaultValue: "Publish Target" })}
id={option.id} </Label>
checked={
{options.map((option) => {
const isAllChecked =
field.value.length ===
options.filter((opt: any) => opt.id !== "all").length;
const isChecked =
option.id === "all" option.id === "all"
? publishedFor.length === ? isAllChecked
options.filter((opt: any) => opt.id !== "all") : field.value.includes(option.id);
.length
: publishedFor.includes(option.id) const handleChange = () => {
} let updated: string[] = [];
onCheckedChange={() => handleCheckboxChange(option.id)}
/> if (option.id === "all") {
<Label htmlFor={option.id}>{option.label}</Label> updated = isAllChecked
? []
: options
.filter((opt: any) => opt.id !== "all")
.map((opt: any) => opt.id);
} else {
updated = isChecked
? field.value.filter((val) => val !== option.id)
: [...field.value, option.id];
if (isAllChecked && option.id !== "all") {
updated = updated.filter((val) => val !== "all");
}
}
field.onChange(updated);
setPublishedFor(updated);
};
return (
<div
key={option.id}
className="flex gap-2 items-center"
>
<Checkbox
id={option.id}
checked={isChecked}
onCheckedChange={handleChange}
/>
<Label htmlFor={option.id}>{option.label}</Label>
</div>
);
})}
{errors.publishedFor && (
<p className="text-red-500 text-sm">
{errors.publishedFor.message}
</p>
)}
</div> </div>
))} </div>
</div> )}
</div> />
</Card> </Card>
<div className="flex flex-row justify-end gap-3"> <div className="flex flex-row justify-end gap-3">
<div className="mt-4"> <div className="mt-4">