This commit is contained in:
hanif salafi 2025-07-13 02:14:39 +07:00
commit e280a68635
16 changed files with 1262 additions and 482 deletions

View File

@ -136,12 +136,12 @@ const AccountListTable = () => {
Tambah Akun Tambah Akun
</Button> </Button>
</Link> </Link>
<Link href="/admin/broadcast/campaign-list/import"> {/* <Link href="/admin/broadcast/campaign-list/import">
<Button color="success" size="md" className="text-sm"> <Button color="success" size="md" className="text-sm">
<UserIcon /> <UserIcon />
Import Akun Import Akun
</Button> </Button>
</Link> </Link> */}
</div> </div>
</div> </div>
<div className="flex justify-end"> <div className="flex justify-end">

View File

@ -44,7 +44,7 @@ const FormSchema = z.object({
.refine((value) => value.some((item) => item), { .refine((value) => value.some((item) => item), {
message: "Required", message: "Required",
}), }),
accountCategory: z.enum(["polri", "jurnalis", "umumu", "ksp"], { accountCategory: z.enum(["polri", "jurnalis", "umum", "ksp"], {
required_error: "Required", required_error: "Required",
}), }),
email: z.string({ email: z.string({

View File

@ -0,0 +1,62 @@
import React, { useRef, useState, useEffect } from "react";
const AudioPlayer = (props: {urlAudio: string}) => {
const audioRef = useRef<HTMLAudioElement>(null);
const [currentTime, setCurrentTime] = useState(0);
const {urlAudio} = props
const playAudio = () => {
audioRef.current?.play();
};
const pauseAudio = () => {
audioRef.current?.pause();
};
const stopAudio = () => {
if (audioRef.current) {
audioRef.current.pause();
audioRef.current.currentTime = 0;
}
};
useEffect(() => {
const audio = audioRef.current;
if (!audio) return;
const updateTime = () => {
setCurrentTime(audio.currentTime);
};
audio.addEventListener("timeupdate", updateTime);
return () => {
audio.removeEventListener("timeupdate", updateTime);
};
}, []);
const formatTime = (time: number) => {
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60)
.toString()
.padStart(2, "0");
return `${minutes}:${seconds}`;
};
return (
<div style={{ textAlign: "center", marginTop: "50px" }}>
<h2>Pemutar Audio</h2>
<audio ref={audioRef} src={urlAudio} />
<div style={{ marginTop: "10px" }}>
<button onClick={playAudio}> Play</button>
<button onClick={pauseAudio}> Pause</button>
<button onClick={stopAudio}> Stop</button>
</div>
<div style={{ marginTop: "10px" }}>
<span>{formatTime(currentTime)}</span>
</div>
</div>
);
};
export default AudioPlayer;

View File

@ -64,6 +64,8 @@ import { useTranslations } from "next-intl";
import SuggestionModal from "@/components/modal/suggestions-modal"; import SuggestionModal from "@/components/modal/suggestions-modal";
import { formatDateToIndonesian } from "@/utils/globals"; import { formatDateToIndonesian } from "@/utils/globals";
import ApprovalHistoryModal from "@/components/modal/approval-history-modal"; import ApprovalHistoryModal from "@/components/modal/approval-history-modal";
import { useDropzone } from "react-dropzone";
import AudioPlayer from "@/components/audio-player";
const imageSchema = z.object({ const imageSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }), title: z.string().min(1, { message: "Judul diperlukan" }),
@ -139,32 +141,54 @@ export default function FormAudioDetail() {
const [selectedPublishers, setSelectedPublishers] = useState<number[]>([]); const [selectedPublishers, setSelectedPublishers] = useState<number[]>([]);
const [description, setDescription] = useState(""); const [description, setDescription] = useState("");
const [main, setMain] = useState<any>([]); const [main, setMain] = useState<any>([]);
const [detailThumb, setDetailThumb] = useState<any>([]);
const [thumbsSwiper, setThumbsSwiper] = useState<any>(null); const [thumbsSwiper, setThumbsSwiper] = useState<any>(null);
const t = useTranslations("Form"); const t = useTranslations("Form");
const [selectedTarget, setSelectedTarget] = useState(""); const [selectedTarget, setSelectedTarget] = useState("");
const [files, setFiles] = useState<FileType[]>([]);
const [rejectedFiles, setRejectedFiles] = useState<number[]>([]); const [rejectedFiles, setRejectedFiles] = useState<number[]>([]);
const [isUserMabesApprover, setIsUserMabesApprover] = useState(false); const [isUserMabesApprover, setIsUserMabesApprover] = useState(false);
const [audioPlaying, setAudioPlaying] = useState<any>(null); const [audioPlaying, setAudioPlaying] = useState<any>(null);
const [filePlacements, setFilePlacements] = useState<string[][]>([]); const [filePlacements, setFilePlacements] = useState<string[][]>([]);
const [files, setFiles] = useState<FileType[]>([]);
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]);
const [detailThumb, setDetailThumb] = useState<string[]>([]);
const waveSurferRef = useRef<any>(null); const waveSurferRef = useRef<any>(null);
const waveSurfersRef = useRef<WaveSurfer[]>([]);
const [isPlayingIndex, setIsPlayingIndex] = useState<number | null>(null);
const [wavesurfer, setWavesurfer] = useState<WaveSurfer>(); const [wavesurfer, setWavesurfer] = useState<WaveSurfer>();
const [isPlaying, setIsPlaying] = useState(false); const [isPlaying, setIsPlaying] = useState(false);
const [approval, setApproval] = useState<any>(); const [approval, setApproval] = useState<any>();
const onReady = (ws: any) => { const onDrop = (acceptedFiles: File[]) => {
setWavesurfer(ws); setUploadedFiles(acceptedFiles);
setIsPlaying(false); const blobUrls = acceptedFiles.map((file) => URL.createObjectURL(file));
setDetailThumb(blobUrls);
}; };
const onPlayPause = () => { const onReady = (ws: WaveSurfer, index: number) => {
wavesurfer && wavesurfer.playPause(); waveSurfersRef.current[index] = ws;
}; };
const onPlayPause = (index: number) => {
waveSurfersRef.current.forEach((ws, i) => {
if (i === index) {
ws.isPlaying() ? ws.pause() : ws.play();
setIsPlayingIndex(ws.isPlaying() ? index : null);
} else {
ws.pause();
}
});
};
const { getRootProps, getInputProps } = useDropzone({
onDrop,
accept: {
"audio/*": [],
},
multiple: true,
});
let fileTypeId = "4"; let fileTypeId = "4";
const { const {
@ -261,8 +285,8 @@ export default function FormAudioDetail() {
}); });
setupPlacementCheck(details?.files?.length); setupPlacementCheck(details?.files?.length);
if (details.publishedForObject) { if (details?.publishedForObject) {
const publisherIds = details.publishedForObject.map( const publisherIds = details?.publishedForObject.map(
(obj: any) => obj.id (obj: any) => obj.id
); );
setSelectedPublishers(publisherIds); setSelectedPublishers(publisherIds);
@ -276,9 +300,9 @@ export default function FormAudioDetail() {
setSelectedTarget(matchingCategory.name); setSelectedTarget(matchingCategory.name);
} }
setSelectedTarget(details.categoryId); // Untuk dropdown setSelectedTarget(details?.categoryId);
const filesData = details.files || []; const filesData = details?.files || [];
const audioFiles = filesData.filter( const audioFiles = filesData.filter(
(file: any) => (file: any) =>
file.contentType && file.contentType.startsWith("video/webm") file.contentType && file.contentType.startsWith("video/webm")
@ -439,9 +463,9 @@ export default function FormAudioDetail() {
const handleAudioPlayPause = (audioSrc: string) => { const handleAudioPlayPause = (audioSrc: string) => {
if (audioPlaying === audioSrc) { if (audioPlaying === audioSrc) {
setAudioPlaying(null); // Pause if the same audio is clicked setAudioPlaying(null);
} else { } else {
setAudioPlaying(audioSrc); // Play the new audio setAudioPlaying(audioSrc);
} }
}; };
@ -463,7 +487,9 @@ export default function FormAudioDetail() {
<div className="flex flex-col lg:flex-row gap-10"> <div className="flex flex-col lg:flex-row gap-10">
<Card className="w-full lg:w-8/12"> <Card className="w-full lg:w-8/12">
<div className="px-6 py-6"> <div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">{t("form-audio", { defaultValue: "Form Audio" })}</p> <p className="text-lg font-semibold mb-3">
{t("form-audio", { defaultValue: "Form Audio" })}
</p>
<div className="gap-5 mb-5"> <div className="gap-5 mb-5">
{/* Input Title */} {/* Input Title */}
<div className="space-y-2 py-3"> <div className="space-y-2 py-3">
@ -491,7 +517,7 @@ export default function FormAudioDetail() {
<div className="py-3 w-full space-y-2"> <div className="py-3 w-full space-y-2">
<Label>{t("category", { defaultValue: "Category" })}</Label> <Label>{t("category", { defaultValue: "Category" })}</Label>
<Select <Select
value={detail?.category.name} // Nilai default berdasarkan detail value={detail?.category.name}
onValueChange={(id) => { onValueChange={(id) => {
console.log("Selected Category:", id); console.log("Selected Category:", id);
setSelectedTarget(id); setSelectedTarget(id);
@ -512,7 +538,9 @@ export default function FormAudioDetail() {
</div> </div>
<div className="py-3 space-y-2"> <div className="py-3 space-y-2">
<Label>{t("description", { defaultValue: "Description" })}</Label> <Label>
{t("description", { defaultValue: "Description" })}
</Label>
<Controller <Controller
control={control} control={control}
name="description" name="description"
@ -527,42 +555,25 @@ export default function FormAudioDetail() {
)} )}
</div> </div>
<Label className="text-xl space-y-2">{t("file-media", { defaultValue: "File Media" })}</Label>
<div className="w-full"> <div className="w-full">
<div className={"container example"}> <Label className="text-xl space-y-2">
{detailThumb?.map((url: any, index: number) => ( {t("file-media", { defaultValue: "File Media" })}
<div key={url.id}> </Label>
<WavesurferPlayer <div className="container example">
height={500} {detailThumb.map((url, index) => (
waveColor="red" <div key={index}>
{/* <WavesurferPlayer
url={url} url={url}
onReady={onReady} waveColor="red"
onPlay={() => setIsPlaying(true)} height={80}
onPause={() => setIsPlaying(false)} onReady={(ws: any) => onReady(ws, index)}
/> />
<Button onClick={() => onPlayPause(index)}>
{isPlayingIndex === index ? "Pause" : "Play"}
</Button> */}
<AudioPlayer urlAudio={url} />
</div> </div>
))} ))}
<Button
size="sm"
type="button"
onClick={onPlayPause}
disabled={isPlaying}
className={`flex items-center gap-2 ${
isPlaying
? "bg-gray-300 cursor-not-allowed"
: "bg-primary text-white"
} p-2 rounded`}
>
{isPlaying ? "Pause" : "Play"}
<Icon
icon={
isPlaying
? "carbon:pause-outline"
: "famicons:play-sharp"
}
className="h-5 w-5"
/>
</Button>
</div> </div>
</div> </div>
</div> </div>
@ -613,7 +624,9 @@ export default function FormAudioDetail() {
</div> </div>
<div className="px-3 py-3"> <div className="px-3 py-3">
<div className="flex flex-col gap-6 space-y-2"> <div className="flex flex-col gap-6 space-y-2">
<Label>{t("publish-target", { defaultValue: "Publish Target" })}</Label> <Label>
{t("publish-target", { defaultValue: "Publish Target" })}
</Label>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<Checkbox <Checkbox
id="5" id="5"
@ -675,7 +688,9 @@ export default function FormAudioDetail() {
<Dialog open={modalOpen} onOpenChange={setModalOpen}> <Dialog open={modalOpen} onOpenChange={setModalOpen}>
<DialogContent size="md"> <DialogContent size="md">
<DialogHeader> <DialogHeader>
<DialogTitle>{t("leave-comment", { defaultValue: "Leave Comment" })}</DialogTitle> <DialogTitle>
{t("leave-comment", { defaultValue: "Leave Comment" })}
</DialogTitle>
</DialogHeader> </DialogHeader>
{status == "2" {status == "2"
? files?.map((file, index) => ( ? files?.map((file, index) => (
@ -893,7 +908,8 @@ export default function FormAudioDetail() {
color="primary" color="primary"
type="button" type="button"
> >
<Icon icon="fa:check" className="mr-3" /> {t("accept", { defaultValue: "Accept" })} <Icon icon="fa:check" className="mr-3" />{" "}
{t("accept", { defaultValue: "Accept" })}
</Button> </Button>
<Button <Button
onClick={() => actionApproval("3")} onClick={() => actionApproval("3")}

View File

@ -156,20 +156,33 @@ export default function FormAudio() {
{ id: "8", label: "KSP" }, { id: "8", label: "KSP" },
]; ];
const audioRefs = useRef<HTMLAudioElement[]>([]);
const { getRootProps, getInputProps } = useDropzone({ const { getRootProps, getInputProps } = useDropzone({
multiple: true,
accept: { "audio/*": [] },
onDrop: (acceptedFiles) => { onDrop: (acceptedFiles) => {
setFiles(acceptedFiles.map((file) => Object.assign(file))); const filesWithPreview = acceptedFiles.map((file) =>
}, Object.assign(file, { preview: URL.createObjectURL(file) })
accept: { );
"audio/*": [], setFiles((prev) => [...prev, ...filesWithPreview]);
}, },
}); });
const handlePlay = (index: number) => {
audioRefs.current.forEach((audio, i) => {
if (audio && i !== index) {
audio.pause();
audio.currentTime = 0;
}
});
};
const audioSchema = z.object({ const audioSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }), title: z.string().min(1, { message: "Judul diperlukan" }),
description: z.string().optional(), description: z.string().optional(),
descriptionOri: z.string().optional(), // Original editor descriptionOri: z.string().optional(),
rewriteDescription: z.string().optional(), // Rewrite editor rewriteDescription: z.string().optional(),
creatorName: z.string().min(1, { message: "Creator diperlukan" }), creatorName: z.string().min(1, { message: "Creator diperlukan" }),
// tags: z.string().min(1, { message: "Judul diperlukan" }), // tags: z.string().min(1, { message: "Judul diperlukan" }),
@ -803,7 +816,9 @@ export default function FormAudio() {
<div className="flex flex-col lg:flex-row gap-10"> <div className="flex flex-col lg:flex-row gap-10">
<Card className="w-full lg:w-8/12"> <Card className="w-full lg:w-8/12">
<div className="px-6 py-6"> <div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">{t("form-audio", { defaultValue: "Form Audio" })}</p> <p className="text-lg font-semibold mb-3">
{t("form-audio", { defaultValue: "Form Audio" })}
</p>
<div className="gap-5 mb-5"> <div className="gap-5 mb-5">
{/* Input Title */} {/* Input Title */}
<div className="space-y-2 py-3"> <div className="space-y-2 py-3">
@ -830,7 +845,7 @@ export default function FormAudio() {
<div className="py-3 w-full space-y-2"> <div className="py-3 w-full space-y-2">
<Label>{t("category", { defaultValue: "Category" })}</Label> <Label>{t("category", { defaultValue: "Category" })}</Label>
<Select <Select
value={selectedCategory} // Ensure selectedTarget is updated correctly value={selectedCategory}
onValueChange={(id) => { onValueChange={(id) => {
console.log("Selected Category ID:", id); console.log("Selected Category ID:", id);
setSelectedCategory(id); setSelectedCategory(id);
@ -853,7 +868,9 @@ export default function FormAudio() {
</div> </div>
</div> </div>
<div className="flex flex-row items-center gap-3 py-2"> <div className="flex flex-row items-center gap-3 py-2">
<Label>{t("ai-assistance", { defaultValue: "Ai Assistance" })}</Label> <Label>
{t("ai-assistance", { defaultValue: "Ai Assistance" })}
</Label>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Switch <Switch
defaultChecked={isSwitchOn} defaultChecked={isSwitchOn}
@ -869,7 +886,9 @@ export default function FormAudio() {
<div> <div>
<div className="flex flex-row gap-3"> <div className="flex flex-row gap-3">
<div className="space-y-2 py-3 w-4/12"> <div className="space-y-2 py-3 w-4/12">
<Label>{t("language", { defaultValue: "Language" })}</Label> <Label>
{t("language", { defaultValue: "Language" })}
</Label>
<Select onValueChange={setSelectedLanguage}> <Select onValueChange={setSelectedLanguage}>
<SelectTrigger size="md"> <SelectTrigger size="md">
<SelectValue placeholder="Pilih" /> <SelectValue placeholder="Pilih" />
@ -881,7 +900,9 @@ export default function FormAudio() {
</Select> </Select>
</div> </div>
<div className="space-y-2 py-3 w-4/12"> <div className="space-y-2 py-3 w-4/12">
<Label>{t("writing-style", { defaultValue: "Writing Style" })}</Label> <Label>
{t("writing-style", { defaultValue: "Writing Style" })}
</Label>
<Select onValueChange={setSelectedWritingStyle}> <Select onValueChange={setSelectedWritingStyle}>
<SelectTrigger size="md"> <SelectTrigger size="md">
<SelectValue placeholder="Pilih" /> <SelectValue placeholder="Pilih" />
@ -900,7 +921,9 @@ export default function FormAudio() {
</Select> </Select>
</div> </div>
<div className="space-y-2 py-3 w-4/12"> <div className="space-y-2 py-3 w-4/12">
<Label>{t("article-size", { defaultValue: "Article Size" })}</Label> <Label>
{t("article-size", { defaultValue: "Article Size" })}
</Label>
<Select onValueChange={setSelectedSize}> <Select onValueChange={setSelectedSize}>
<SelectTrigger size="md"> <SelectTrigger size="md">
<SelectValue placeholder="Pilih" /> <SelectValue placeholder="Pilih" />
@ -921,7 +944,9 @@ export default function FormAudio() {
</div> </div>
<div className="mt-5"> <div className="mt-5">
<div className="flex flex-row items-center gap-3 mb-3"> <div className="flex flex-row items-center gap-3 mb-3">
<Label>{t("main-keyword", { defaultValue: "Main Keyword" })}</Label> <Label>
{t("main-keyword", { defaultValue: "Main Keyword" })}
</Label>
<Button <Button
variant="outline" variant="outline"
color="primary" color="primary"
@ -978,9 +1003,13 @@ export default function FormAudio() {
</Button> </Button>
</div> </div>
<p className="font-semibold"> <p className="font-semibold">
{t("Keywords to include in the text", { defaultValue: "Keywords To Include In The Text" })} {t("Keywords to include in the text", {
defaultValue: "Keywords To Include In The Text",
})}
</p>
<p className="text-sm">
{t("title-key", { defaultValue: "Title Key" })}
</p> </p>
<p className="text-sm">{t("title-key", { defaultValue: "Title Key" })}</p>
<div className="mt-3"> <div className="mt-3">
<Textarea <Textarea
value={selectedSEO} value={selectedSEO}
@ -990,7 +1019,12 @@ export default function FormAudio() {
</div> </div>
</div> </div>
<div className="mt-5"> <div className="mt-5">
<Label>{t("special-instructions", { defaultValue: "Special Instructions" })}(Optional)</Label> <Label>
{t("special-instructions", {
defaultValue: "Special Instructions",
})}
(Optional)
</Label>
<div className="mt-3"> <div className="mt-3">
<Controller <Controller
control={control} control={control}
@ -1056,7 +1090,9 @@ export default function FormAudio() {
</div> </div>
</div> </div>
<div className="py-3 space-y-2"> <div className="py-3 space-y-2">
<Label>{t("description", { defaultValue: "Description" })}</Label> <Label>
{t("description", { defaultValue: "Description" })}
</Label>
<Controller <Controller
control={control} control={control}
name="description" name="description"
@ -1100,7 +1136,9 @@ export default function FormAudio() {
</Label> </Label>
</div> </div>
<div className="py-3 space-y-2"> <div className="py-3 space-y-2">
<Label>{t("description", { defaultValue: "Description" })}</Label> <Label>
{t("description", { defaultValue: "Description" })}
</Label>
<Controller <Controller
control={control} control={control}
name="descriptionOri" name="descriptionOri"
@ -1160,7 +1198,11 @@ export default function FormAudio() {
</Label> </Label>
</div> </div>
<div className="py-3 space-y-2"> <div className="py-3 space-y-2">
<Label>{t("file-rewrite", { defaultValue: "File Rewrite" })}</Label> <Label>
{t("file-rewrite", {
defaultValue: "File Rewrite",
})}
</Label>
<Controller <Controller
control={control} control={control}
name="rewriteDescription" name="rewriteDescription"
@ -1189,36 +1231,51 @@ export default function FormAudio() {
</> </>
)} )}
<div className="py-3 space-y-2"> <div className="py-3 space-y-2">
<Label>{t("select-file", { defaultValue: "Select File" })}</Label> <Label>
{/* <Input {t("select-file", { defaultValue: "Select File" })}
id="fileInput" </Label>
type="file"
onChange={handleImageChange}
/> */}
<Fragment> <Fragment>
<div {...getRootProps({ className: "dropzone" })}> <div {...getRootProps({ className: "dropzone" })}>
<input {...getInputProps()} /> <input {...getInputProps({ multiple: true })} />
<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"> <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" /> <CloudUpload className="text-default-300 w-10 h-10" />
<h4 className=" text-2xl font-medium mb-1 mt-3 text-card-foreground/80"> <h4 className="text-2xl font-medium mb-1 mt-3 text-card-foreground/80">
{/* Drop files here or click to upload. */}
{t("drag-file", { defaultValue: "Drag File" })} {t("drag-file", { defaultValue: "Drag File" })}
</h4> </h4>
<div className=" text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
{t("upload-file-audio-max", { defaultValue: "Upload File Audio Max" })} {t("upload-file-audio-max", {
defaultValue: "Upload File Audio Max",
})}
</div> </div>
</div> </div>
</div> </div>
{files.length ? ( {files.length ? (
<Fragment> <Fragment>
<div>{fileList}</div> <div className="space-y-4 mt-4">
<div className=" flex justify-between gap-2"> {files.map((file, idx) => (
{/* <div className="flex flex-row items-center gap-3 py-3"> <div
<Label>Gunakan Watermark</Label> key={idx}
<div className="flex items-center gap-3"> className="flex flex-col gap-2 border p-2 rounded-md"
<Switch defaultChecked color="primary" id="c2" /> >
<p className="text-sm font-medium truncate">
{file.name}
</p>
<audio
controls
src={file.preview ?? URL.createObjectURL(file)}
ref={(el) => {
if (el) audioRefs.current[idx] = el;
}}
onPlay={() => handlePlay(idx)}
className="w-full rounded"
>
Your browser does not support the audio element.
</audio>
</div> </div>
</div> */} ))}
</div>
<div className="flex justify-between gap-2 mt-3">
<Button <Button
color="destructive" color="destructive"
onClick={handleRemoveAllFiles} onClick={handleRemoveAllFiles}
@ -1259,7 +1316,9 @@ export default function FormAudio() {
</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">{t("tags", { defaultValue: "Tags" })}</Label> <Label htmlFor="tags">
{t("tags", { defaultValue: "Tags" })}
</Label>
<Input <Input
type="text" type="text"
@ -1288,7 +1347,9 @@ export default function FormAudio() {
</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>{t("publish-target", { defaultValue: "Publish Target" })}</Label> <Label>
{t("publish-target", { defaultValue: "Publish Target" })}
</Label>
{options.map((option) => ( {options.map((option) => (
<div key={option.id} className="flex gap-2 items-center"> <div key={option.id} className="flex gap-2 items-center">
<Checkbox <Checkbox

View File

@ -0,0 +1,59 @@
import React from "react";
type FileData = {
url: string;
format: string; // extension with dot, e.g. ".pdf"
fileName?: string;
};
interface FilePreviewProps {
file: FileData;
}
const FileTextPreview: React.FC<FilePreviewProps> = ({ file }) => {
const format = file.format.toLowerCase();
if ([".jpg", ".jpeg", ".png", ".webp"].includes(format)) {
return (
<img
className="object-fill h-full w-full rounded-md"
src={file.url}
alt={file.fileName || "File"}
/>
);
}
if (format === ".pdf") {
return (
<iframe
className="w-full h-96 rounded-md"
src={`https://drive.google.com/viewerng/viewer?embedded=true&url=${encodeURIComponent(file.url)}`}
title={file.fileName || "PDF File"}
/>
);
}
if ([".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx"].includes(format)) {
return (
<iframe
className="w-full h-96 rounded-md"
src={`https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(file.url)}`}
title={file.fileName || "Document"}
/>
);
}
// Fallback → unknown format
return (
<a
href={file.url}
target="_blank"
rel="noopener noreferrer"
className="block text-blue-500 underline"
>
View {file.fileName || "File"}
</a>
);
};
export default FileTextPreview;

View File

@ -0,0 +1,33 @@
import React from "react";
type FileData = {
url: string;
format: string;
fileName?: string;
};
interface FileThumbnailProps {
file: FileData;
}
const FileTextThumbnail: React.FC<FileThumbnailProps> = ({ file }) => {
const format = file.format.toLowerCase();
if ([".jpg", ".jpeg", ".png", ".webp"].includes(format)) {
return (
<img
className="object-cover h-[60px] w-[80px] rounded-md"
src={file.url}
alt={file.fileName}
/>
);
}
return (
<div className="h-[60px] w-[80px] flex items-center justify-center bg-gray-200 text-sm text-center text-gray-700 rounded-md">
{format.replace(".", "").toUpperCase()}
</div>
);
};
export default FileTextThumbnail;

View File

@ -88,7 +88,6 @@ export default function FormImage() {
type ImageSchema = z.infer<typeof imageSchema>; type ImageSchema = z.infer<typeof imageSchema>;
const params = useParams(); const params = useParams();
const locale = params?.locale; const locale = params?.locale;
const t = useTranslations("Form"); const t = useTranslations("Form");
const [selectedFiles, setSelectedFiles] = useState<File[]>([]); const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const taskId = Cookies.get("taskId"); const taskId = Cookies.get("taskId");
@ -171,11 +170,17 @@ export default function FormImage() {
}); });
const imageSchema = z.object({ const imageSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }), 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: "Creator diperlukan" }), creatorName: z.string().min(1, { message: t("creatorRequired") }),
categoryId: z.string().min(1, { message: "Kategori diperlukan" }),
tags: z.array(z.string()).min(1, { message: "Minimal 1 tag diperlukan" }),
publishedFor: z
.array(z.string())
.min(1, { message: "Pilih target publish" }),
files: z.array(z.any()).min(1, { message: "File harus diupload" }),
}); });
const { const {
@ -183,6 +188,7 @@ export default function FormImage() {
handleSubmit, handleSubmit,
getValues, getValues,
setValue, setValue,
watch,
formState: { errors }, formState: { errors },
} = useForm<ImageSchema>({ } = useForm<ImageSchema>({
resolver: zodResolver(imageSchema), resolver: zodResolver(imageSchema),
@ -190,6 +196,12 @@ export default function FormImage() {
description: "", description: "",
descriptionOri: "", descriptionOri: "",
rewriteDescription: "", rewriteDescription: "",
title: "",
creatorName: "",
categoryId: "",
tags: [],
publishedFor: [],
files: [],
}, },
}); });
@ -232,10 +244,10 @@ export default function FormImage() {
} }
} else { } else {
Swal.fire({ Swal.fire({
icon: "warning", icon: "warning",
title: "WARNING", title: "WARNING",
text: "Please provide a valid main keyword.", text: "Please provide a valid main keyword.",
}); });
console.error("Please provide a valid main keyword."); console.error("Please provide a valid main keyword.");
} }
}; };
@ -263,11 +275,11 @@ export default function FormImage() {
setIsLoading(false); setIsLoading(false);
} }
} else { } else {
Swal.fire({ Swal.fire({
icon: "warning", icon: "warning",
title: "WARNING", title: "WARNING",
text: "Please provide a valid title.", text: "Please provide a valid title.",
}); });
console.error("Please provide a valid main keyword."); console.error("Please provide a valid main keyword.");
} }
}; };
@ -295,11 +307,11 @@ export default function FormImage() {
setIsLoading(false); setIsLoading(false);
} }
} else { } else {
Swal.fire({ Swal.fire({
icon: "warning", icon: "warning",
title: "WARNING", title: "WARNING",
text: "Please provide a valid keyword.", text: "Please provide a valid keyword.",
}); });
console.error("Please provide a valid main keyword."); console.error("Please provide a valid main keyword.");
} }
}; };
@ -785,7 +797,9 @@ export default function FormImage() {
<div className="flex flex-col lg:flex-row gap-10"> <div className="flex flex-col lg:flex-row gap-10">
<Card className="w-full lg:w-8/12"> <Card className="w-full lg:w-8/12">
<div className="px-6 py-6"> <div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">{t("form-image", { defaultValue: "Form Image" })}</p> <p className="text-lg font-semibold mb-3">
{t("form-image", { defaultValue: "Form Image" })}
</p>
<div className="gap-5 mb-5"> <div className="gap-5 mb-5">
{/* Input Title */} {/* Input Title */}
<div className="space-y-2 py-3"> <div className="space-y-2 py-3">
@ -799,7 +813,9 @@ export default function FormImage() {
type="text" type="text"
value={field.value} value={field.value}
onChange={field.onChange} onChange={field.onChange}
placeholder="Enter Title" placeholder={t("enterTitle", {
defaultValue: "Masukkan Title",
})}
/> />
)} )}
/> />
@ -811,7 +827,7 @@ export default function FormImage() {
<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);
@ -831,11 +847,39 @@ 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>
<div className="flex flex-row items-center gap-3 py-3 "> <div className="flex flex-row items-center gap-3 py-3 ">
<Label>{t("ai-assistance", { defaultValue: "Ai Assistance" })}</Label> <Label>
{t("ai-assistance", { defaultValue: "Ai Assistance" })}
</Label>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Switch <Switch
defaultChecked={isSwitchOn} defaultChecked={isSwitchOn}
@ -851,7 +895,9 @@ export default function FormImage() {
<div> <div>
<div className="flex flex-row gap-3"> <div className="flex flex-row gap-3">
<div className="space-y-2 py-3 w-4/12"> <div className="space-y-2 py-3 w-4/12">
<Label>{t("language", { defaultValue: "Language" })}</Label> <Label>
{t("language", { defaultValue: "Language" })}
</Label>
<Select onValueChange={setSelectedLanguage}> <Select onValueChange={setSelectedLanguage}>
<SelectTrigger size="md"> <SelectTrigger size="md">
<SelectValue placeholder="Pilih" /> <SelectValue placeholder="Pilih" />
@ -863,7 +909,9 @@ export default function FormImage() {
</Select> </Select>
</div> </div>
<div className="space-y-2 py-3 w-4/12"> <div className="space-y-2 py-3 w-4/12">
<Label>{t("writing-style", { defaultValue: "Writing Style" })}</Label> <Label>
{t("writing-style", { defaultValue: "Writing Style" })}
</Label>
<Select onValueChange={setSelectedWritingStyle}> <Select onValueChange={setSelectedWritingStyle}>
<SelectTrigger size="md"> <SelectTrigger size="md">
<SelectValue placeholder="Pilih" /> <SelectValue placeholder="Pilih" />
@ -882,7 +930,9 @@ export default function FormImage() {
</Select> </Select>
</div> </div>
<div className="space-y-2 py-3 w-4/12"> <div className="space-y-2 py-3 w-4/12">
<Label>{t("article-size", { defaultValue: "Article Size" })}</Label> <Label>
{t("article-size", { defaultValue: "Article Size" })}
</Label>
<Select onValueChange={setSelectedSize}> <Select onValueChange={setSelectedSize}>
<SelectTrigger size="md"> <SelectTrigger size="md">
<SelectValue placeholder="Pilih" /> <SelectValue placeholder="Pilih" />
@ -903,7 +953,9 @@ export default function FormImage() {
</div> </div>
<div className="mt-5"> <div className="mt-5">
<div className="flex flex-row items-center gap-3 mb-3"> <div className="flex flex-row items-center gap-3 mb-3">
<Label>{t("main-keyword", { defaultValue: "Main Keyword" })}</Label> <Label>
{t("main-keyword", { defaultValue: "Main Keyword" })}
</Label>
<Button <Button
variant="outline" variant="outline"
color="primary" color="primary"
@ -960,9 +1012,13 @@ export default function FormImage() {
</Button> </Button>
</div> </div>
<p className="font-semibold"> <p className="font-semibold">
{t("Keywords to include in the text", { defaultValue: "Keywords To Include In The Text" })} {t("Keywords to include in the text", {
defaultValue: "Keywords To Include In The Text",
})}
</p>
<p className="text-sm">
{t("title-key", { defaultValue: "Title Key" })}
</p> </p>
<p className="text-sm">{t("title-key", { defaultValue: "Title Key" })}</p>
<div className="mt-3"> <div className="mt-3">
<Textarea <Textarea
value={selectedSEO} value={selectedSEO}
@ -972,7 +1028,12 @@ export default function FormImage() {
</div> </div>
</div> </div>
<div className="mt-5"> <div className="mt-5">
<Label>{t("special-instructions", { defaultValue: "Special Instructions" })}(Optional)</Label> <Label>
{t("special-instructions", {
defaultValue: "Special Instructions",
})}
(Optional)
</Label>
<div className="mt-3"> <div className="mt-3">
<Controller <Controller
control={control} control={control}
@ -1005,7 +1066,7 @@ export default function FormImage() {
{articleIds.map((id: string, index: number) => ( {articleIds.map((id: string, index: number) => (
<p <p
key={index} key={index}
className={`mr-3 px-3 py-2 rounded-md ${ className={`mr-3 px-3 py-2 rounded-md cursor-pointer ${
selectedArticleId === id selectedArticleId === id
? "bg-green-500 text-white" ? "bg-green-500 text-white"
: "border-2 border-green-500 text-green-500" : "border-2 border-green-500 text-green-500"
@ -1038,7 +1099,9 @@ export default function FormImage() {
</div> </div>
</div> </div>
<div className="py-3 space-y-2"> <div className="py-3 space-y-2">
<Label>{t("description", { defaultValue: "Description" })}</Label> <Label>
{t("description", { defaultValue: "Description" })}
</Label>
<Controller <Controller
control={control} control={control}
name="description" name="description"
@ -1082,7 +1145,9 @@ export default function FormImage() {
</Label> </Label>
</div> </div>
<div className="py-3 space-y-2"> <div className="py-3 space-y-2">
<Label>{t("description", { defaultValue: "Description" })}</Label> <Label>
{t("description", { defaultValue: "Description" })}
</Label>
<Controller <Controller
control={control} control={control}
name="descriptionOri" name="descriptionOri"
@ -1142,7 +1207,11 @@ export default function FormImage() {
</Label> </Label>
</div> </div>
<div className="py-3 space-y-2"> <div className="py-3 space-y-2">
<Label>{t("file-rewrite", { defaultValue: "File Rewrite" })}</Label> <Label>
{t("file-rewrite", {
defaultValue: "File Rewrite",
})}
</Label>
<Controller <Controller
control={control} control={control}
name="rewriteDescription" name="rewriteDescription"
@ -1170,53 +1239,163 @@ export default function FormImage() {
</RadioGroup> </RadioGroup>
</> </>
)} )}
<div className="py-3 space-y-2">
<Label>{t("select-file", { defaultValue: "Select File" })}</Label> <div>
{/* <Input <Controller
id="fileInput" control={control}
type="file" name="files"
onChange={handleImageChange} render={({ field }) => {
/> */} const maxSize = 100 * 1024 * 1024;
<Fragment>
<div {...getRootProps({ className: "dropzone" })}> const { getRootProps, getInputProps, fileRejections } =
<input {...getInputProps()} /> useDropzone({
<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"> accept: {
<CloudUpload className="text-default-300 w-10 h-10" /> "image/jpeg": [".jpeg", ".jpg"],
<h4 className=" text-2xl font-medium mb-1 mt-3 text-card-foreground/80"> "image/png": [".png"],
{/* Drop files here or click to upload. */} },
{t("drag-file", { defaultValue: "Drag File" })} maxSize,
</h4> multiple: false,
<div className=" text-xs text-muted-foreground"> onDrop: (acceptedFiles) => {
{t("upload-file-max", { defaultValue: "Upload File Max" })} field.onChange(acceptedFiles);
},
});
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 && (
<>
<ul className="mt-2 text-sm">
{field.value.map((file, idx) => (
<li key={idx}>
{file.name} (
{(file.size / (1024 * 1024)).toFixed(2)} MB)
</li>
))}
</ul>
<div className="flex justify-end mt-2">
<Button
type="button"
color="destructive"
onClick={() => field.onChange([])}
>
{t("remove-all", {
defaultValue: "Remove All",
})}
</Button>
</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> </div>
</div>
{files.length ? (
<Fragment>
<div>{fileList}</div>
<div className=" flex justify-between gap-2">
{/* <div className="flex flex-row items-center gap-3 py-3">
<Label>{t("watermark", { defaultValue: "Watermark" })}</Label>
<div className="flex items-center gap-3">
<Switch defaultChecked color="primary" id="c2" />
</div>
</div> */}
<Button
color="destructive"
onClick={handleRemoveAllFiles}
>
{t("remove-all", { defaultValue: "Remove All" })}
</Button>
</div>
</Fragment>
) : null}
</Fragment>
</div>
</div>
{/* Submit Button */} {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> </div>
</Card> </Card>
<div className="w-full lg:w-4/12"> <div className="w-full lg:w-4/12">
<Card className=" h-[500px]"> <Card className=" h-[500px]">
<div className="px-3 py-3"> <div className="px-3 py-3">
@ -1242,8 +1421,10 @@ 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">{t("tags", { defaultValue: "Tags" })}</Label> <Label htmlFor="tags">
{t("tags", { defaultValue: "Tags" })}
</Label>
<Input <Input
type="text" type="text"
id="tags" id="tags"
@ -1268,10 +1449,70 @@ export default function FormImage() {
</span> </span>
))} ))}
</div> </div>
</div> */}
<div className="px-3 py-3 space-y-2">
<Label htmlFor="tags">
{t("tags", { defaultValue: "Tags" })}
</Label>
<Controller
control={control}
name="tags"
render={({ field }) => (
<>
<Input
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?.message && (
<p className="text-red-400 text-sm">{errors.tags.message}</p>
)}
</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>{t("publish-target", { defaultValue: "Publish Target" })}</Label> <Label>
{t("publish-target", { defaultValue: "Publish Target" })}
</Label>
{options.map((option) => ( {options.map((option) => (
<div key={option.id} className="flex gap-2 items-center"> <div key={option.id} className="flex gap-2 items-center">
<Checkbox <Checkbox
@ -1289,6 +1530,62 @@ export default function FormImage() {
</div> </div>
))} ))}
</div> </div>
</div> */}
<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
control={control}
name="publishedFor"
render={({ field }) => (
<>
{options.map((option) => (
<div
key={option.id}
className="flex gap-2 items-center"
>
<Checkbox
id={option.id}
checked={field.value.includes(option.id)}
onCheckedChange={(checked: boolean) => {
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>
</div>
))}
{/* Tampilkan error */}
{errors.publishedFor?.message && (
<p className="text-red-400 text-sm">
{errors.publishedFor.message}
</p>
)}
</>
)}
/>
</div>
</div> </div>
</Card> </Card>
<div className="flex flex-row justify-end gap-3"> <div className="flex flex-row justify-end gap-3">

View File

@ -62,6 +62,8 @@ import { useTranslations } from "next-intl";
import SuggestionModal from "@/components/modal/suggestions-modal"; import SuggestionModal from "@/components/modal/suggestions-modal";
import { formatDateToIndonesian } from "@/utils/globals"; import { formatDateToIndonesian } from "@/utils/globals";
import ApprovalHistoryModal from "@/components/modal/approval-history-modal"; import ApprovalHistoryModal from "@/components/modal/approval-history-modal";
import FileTextPreview from "./file-preview-text";
import FileTextThumbnail from "./file-text-thumbnail";
const imageSchema = z.object({ const imageSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }), title: z.string().min(1, { message: "Judul diperlukan" }),
@ -435,7 +437,9 @@ export default function FormTeksDetail() {
<div className="flex flex-col lg:flex-row gap-10"> <div className="flex flex-col lg:flex-row gap-10">
<Card className="w-full lg:w-8/12"> <Card className="w-full lg:w-8/12">
<div className="px-6 py-6"> <div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">{t("form-text", { defaultValue: "Form Text" })}</p> <p className="text-lg font-semibold mb-3">
{t("form-text", { defaultValue: "Form Text" })}
</p>
<div className="gap-5 mb-5"> <div className="gap-5 mb-5">
{/* Input Title */} {/* Input Title */}
<div className="space-y-2 py-3"> <div className="space-y-2 py-3">
@ -484,7 +488,9 @@ export default function FormTeksDetail() {
</div> </div>
<div className="py-3 space-y-2"> <div className="py-3 space-y-2">
<Label>{t("description", { defaultValue: "Description" })}</Label> <Label>
{t("description", { defaultValue: "Description" })}
</Label>
<Controller <Controller
control={control} control={control}
name="description" name="description"
@ -499,7 +505,9 @@ export default function FormTeksDetail() {
)} )}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-xl">{t("file-media", { defaultValue: "File Media" })} </Label> <Label className="text-xl">
{t("file-media", { defaultValue: "File Media" })}{" "}
</Label>
<div className="w-full"> <div className="w-full">
<Swiper <Swiper
thumbs={{ swiper: thumbsSwiper }} thumbs={{ swiper: thumbsSwiper }}
@ -507,50 +515,14 @@ export default function FormTeksDetail() {
navigation={false} navigation={false}
className="w-full" className="w-full"
> >
{detailThumb?.map((data: any, index: number) => ( {detailThumb?.map((file: any, index: any) => (
<SwiperSlide key={index}> <SwiperSlide key={index}>
{[".jpg", ".jpeg", ".png", ".webp"].includes( <FileTextPreview file={file} />
data.format
) ? (
// Menampilkan gambar
<img
className="object-fill h-full w-full rounded-md"
src={data.url}
alt={data.fileName || "File"}
/>
) : data.format === ".pdf" ? (
// Menampilkan PDF menggunakan iframe
<iframe
className="w-full h-96 rounded-md"
src={data.url}
title={data.fileName || "PDF File"}
/>
) : [".docx", ".ppt", ".pptx"].includes(
data.format
) ? (
// Menampilkan file dokumen menggunakan Office Viewer
<iframe
className="w-full h-96 rounded-md"
src={`https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(
data.url
)}`}
title={data.fileName || "Document"}
/>
) : (
// Menampilkan link jika format tidak dikenali
<a
href={data.url}
target="_blank"
rel="noopener noreferrer"
className="block text-blue-500 underline"
>
View {data.fileName || "File"}
</a>
)}
</SwiperSlide> </SwiperSlide>
))} ))}
</Swiper> </Swiper>
<div className="mt-2 ">
<div className="mt-2">
<Swiper <Swiper
onSwiper={setThumbsSwiper} onSwiper={setThumbsSwiper}
slidesPerView={8} slidesPerView={8}
@ -558,21 +530,9 @@ export default function FormTeksDetail() {
pagination={{ clickable: true }} pagination={{ clickable: true }}
modules={[Pagination, Thumbs]} modules={[Pagination, Thumbs]}
> >
{detailThumb?.map((data: any, index: number) => ( {detailThumb?.map((file: any, index: any) => (
<SwiperSlide key={index}> <SwiperSlide key={index}>
{[".jpg", ".jpeg", ".png", ".webp"].includes( <FileTextThumbnail file={file} />
data.format
) ? (
<img
className="object-cover h-[60px] w-[80px]"
src={data.url}
alt={data.fileName}
/>
) : (
<div className="h-[60px] w-[80px] flex items-center justify-center bg-gray-200 text-sm text-center text-gray-700 rounded-md">
{data?.format?.replace(".", "").toUpperCase()}
</div>
)}
</SwiperSlide> </SwiperSlide>
))} ))}
</Swiper> </Swiper>
@ -636,7 +596,9 @@ export default function FormTeksDetail() {
</div> </div>
<div className="px-3 py-3"> <div className="px-3 py-3">
<div className="flex flex-col gap-6 space-y-2"> <div className="flex flex-col gap-6 space-y-2">
<Label>{t("publish-target", { defaultValue: "Publish Target" })}</Label> <Label>
{t("publish-target", { defaultValue: "Publish Target" })}
</Label>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<Checkbox <Checkbox
id="5" id="5"
@ -698,7 +660,9 @@ export default function FormTeksDetail() {
<Dialog open={modalOpen} onOpenChange={setModalOpen}> <Dialog open={modalOpen} onOpenChange={setModalOpen}>
<DialogContent size="md"> <DialogContent size="md">
<DialogHeader> <DialogHeader>
<DialogTitle>{t("leave-comment", { defaultValue: "Leave Comment" })}</DialogTitle> <DialogTitle>
{t("leave-comment", { defaultValue: "Leave Comment" })}
</DialogTitle>
</DialogHeader> </DialogHeader>
{status == "2" {status == "2"
? files?.map((file, index) => ( ? files?.map((file, index) => (
@ -916,7 +880,8 @@ export default function FormTeksDetail() {
color="primary" color="primary"
type="button" type="button"
> >
<Icon icon="fa:check" className="mr-3" /> {t("accept", { defaultValue: "Accept" })} <Icon icon="fa:check" className="mr-3" />{" "}
{t("accept", { defaultValue: "Accept" })}
</Button> </Button>
<Button <Button
onClick={() => actionApproval("3")} onClick={() => actionApproval("3")}

View File

@ -154,24 +154,72 @@ export default function FormTeks() {
]; ];
const { getRootProps, getInputProps } = useDropzone({ const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) => {
setFiles(acceptedFiles.map((file) => Object.assign(file)));
},
accept: { accept: {
"application/pdf": [], "application/pdf": [],
"application/msword": [], // .doc "application/msword": [],
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
[], // .docx [],
"application/vnd.ms-powerpoint": [],
"application/vnd.openxmlformats-officedocument.presentationml.presentation":
[],
},
maxSize: 20 * 1024 * 1024,
onDrop: (acceptedFiles) => {
const filtered = acceptedFiles.filter(
(file) =>
[
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
].includes(file.type) && file.size <= 20 * 1024 * 1024
);
const filesWithPreview = filtered.map((file) =>
Object.assign(file, {
preview: URL.createObjectURL(file),
})
);
setFiles(filesWithPreview);
setValue("files", filesWithPreview);
}, },
}); });
const teksSchema = z.object({ const teksSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }), title: z.string().min(1, { message: "Judul diperlukan" }),
description: z.string().optional(),
descriptionOri: z.string().optional(), // Original editor
rewriteDescription: z.string().optional(),
creatorName: z.string().min(1, { message: "Creator diperlukan" }), creatorName: z.string().min(1, { message: "Creator diperlukan" }),
// tags: z.string().min(1, { message: "Judul diperlukan" }), category: z.string().min(1, { message: "Kategori harus dipilih" }),
tags: z
.array(z.string().min(1))
.min(1, { message: "Minimal 1 tag diperlukan" }),
files: z
.array(
z
.object({
name: z.string(),
type: z.string(),
size: z
.number()
.max(20 * 1024 * 1024, { message: "Max file size 20 MB" }),
})
.refine(
(file) =>
[
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
].includes(file.type),
{ message: "Format file tidak didukung" }
)
)
.min(1, { message: "File wajib diunggah" }),
description: z.string().optional(),
descriptionOri: z.string().optional(),
rewriteDescription: z.string().optional(),
}); });
const { const {
@ -183,6 +231,11 @@ export default function FormTeks() {
} = useForm<TeksSchema>({ } = useForm<TeksSchema>({
resolver: zodResolver(teksSchema), resolver: zodResolver(teksSchema),
defaultValues: { defaultValues: {
title: "",
creatorName: "",
category: "",
tags: [],
files: [],
description: "", description: "",
descriptionOri: "", descriptionOri: "",
rewriteDescription: "", rewriteDescription: "",
@ -381,21 +434,22 @@ export default function FormTeks() {
} }
}; };
const handleAddTag = (e: React.KeyboardEvent<HTMLInputElement>) => { const handleAddTag = (
if (e.key === "Enter" && e.currentTarget.value.trim()) { e: React.KeyboardEvent<HTMLInputElement>,
field: any
) => {
if (e.key === "Enter") {
e.preventDefault(); e.preventDefault();
const newTag = e.currentTarget.value.trim(); const value = e.currentTarget.value.trim();
if (!tags.includes(newTag)) { if (value && !field.value.includes(value)) {
setTags((prevTags) => [...prevTags, newTag]); // Add new tag field.onChange([...field.value, value]);
if (inputRef.current) { e.currentTarget.value = "";
inputRef.current.value = ""; // Clear input field
}
} }
} }
}; };
const handleRemoveTag = (index: number, field: any) => {
const handleRemoveTag = (index: number) => { const newTags = field.value.filter((_: any, i: number) => i !== index);
setTags((prevTags) => prevTags.filter((_, i) => i !== index)); // Remove tag field.onChange(newTags);
}; };
const handleRemoveImage = (index: number) => { const handleRemoveImage = (index: number) => {
@ -792,7 +846,9 @@ export default function FormTeks() {
<div className="flex flex-col lg:flex-row gap-10"> <div className="flex flex-col lg:flex-row gap-10">
<Card className="w-full lg:w-8/12"> <Card className="w-full lg:w-8/12">
<div className="px-6 py-6"> <div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">{t("form-text", { defaultValue: "Form Text" })}</p> <p className="text-lg font-semibold mb-3">
{t("form-text", { defaultValue: "Form Text" })}
</p>
<div className="gap-5 mb-5"> <div className="gap-5 mb-5">
{/* Input Title */} {/* Input Title */}
<div className="space-y-2 py-3"> <div className="space-y-2 py-3">
@ -818,31 +874,44 @@ export default function FormTeks() {
<div className="flex items-center"> <div className="flex items-center">
<div className="py-3 w-full space-y-2"> <div className="py-3 w-full space-y-2">
<Label>{t("category", { defaultValue: "Category" })}</Label> <Label>{t("category", { defaultValue: "Category" })}</Label>
<Select <Controller
value={selectedCategory} // Ensure selectedTarget is updated correctly control={control}
onValueChange={(id) => { name="category"
console.log("Selected Category ID:", id); render={({ field }) => (
setSelectedCategory(id); <Select
}} value={field.value}
> onValueChange={(val) => {
<SelectTrigger size="md"> field.onChange(val);
<SelectValue placeholder="Pilih" /> setSelectedCategory(val);
</SelectTrigger> }}
<SelectContent> >
{categories.map((category) => ( <SelectTrigger size="md">
<SelectItem <SelectValue placeholder="Pilih" />
key={category.id} </SelectTrigger>
value={category.id.toString()} <SelectContent>
> {categories.map((category) => (
{category.name} <SelectItem
</SelectItem> key={category.id}
))} value={category.id.toString()}
</SelectContent> >
</Select> {category.name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
{errors.category?.message && (
<p className="text-red-400 text-sm">
{errors.category.message}
</p>
)}
</div> </div>
</div> </div>
<div className="flex flex-row items-center gap-3 py-2"> <div className="flex flex-row items-center gap-3 py-2">
<Label>{t("ai-assistance", { defaultValue: "Ai Assistance" })}</Label> <Label>
{t("ai-assistance", { defaultValue: "Ai Assistance" })}
</Label>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Switch <Switch
defaultChecked={isSwitchOn} defaultChecked={isSwitchOn}
@ -858,7 +927,9 @@ export default function FormTeks() {
<div> <div>
<div className="flex flex-row gap-3"> <div className="flex flex-row gap-3">
<div className="space-y-2 py-3 w-4/12"> <div className="space-y-2 py-3 w-4/12">
<Label>{t("language", { defaultValue: "Language" })}</Label> <Label>
{t("language", { defaultValue: "Language" })}
</Label>
<Select onValueChange={setSelectedLanguage}> <Select onValueChange={setSelectedLanguage}>
<SelectTrigger size="md"> <SelectTrigger size="md">
<SelectValue placeholder="Pilih" /> <SelectValue placeholder="Pilih" />
@ -870,7 +941,9 @@ export default function FormTeks() {
</Select> </Select>
</div> </div>
<div className="space-y-2 py-3 w-4/12"> <div className="space-y-2 py-3 w-4/12">
<Label>{t("writing-style", { defaultValue: "Writing Style" })}</Label> <Label>
{t("writing-style", { defaultValue: "Writing Style" })}
</Label>
<Select onValueChange={setSelectedWritingStyle}> <Select onValueChange={setSelectedWritingStyle}>
<SelectTrigger size="md"> <SelectTrigger size="md">
<SelectValue placeholder="Pilih" /> <SelectValue placeholder="Pilih" />
@ -889,7 +962,9 @@ export default function FormTeks() {
</Select> </Select>
</div> </div>
<div className="space-y-2 py-3 w-4/12"> <div className="space-y-2 py-3 w-4/12">
<Label>{t("article-size", { defaultValue: "Article Size" })}</Label> <Label>
{t("article-size", { defaultValue: "Article Size" })}
</Label>
<Select onValueChange={setSelectedSize}> <Select onValueChange={setSelectedSize}>
<SelectTrigger size="md"> <SelectTrigger size="md">
<SelectValue placeholder="Pilih" /> <SelectValue placeholder="Pilih" />
@ -910,7 +985,9 @@ export default function FormTeks() {
</div> </div>
<div className="mt-5"> <div className="mt-5">
<div className="flex flex-row items-center gap-3 mb-3"> <div className="flex flex-row items-center gap-3 mb-3">
<Label>{t("main-keyword", { defaultValue: "Main Keyword" })}</Label> <Label>
{t("main-keyword", { defaultValue: "Main Keyword" })}
</Label>
<Button <Button
variant="outline" variant="outline"
color="primary" color="primary"
@ -967,9 +1044,13 @@ export default function FormTeks() {
</Button> </Button>
</div> </div>
<p className="font-semibold"> <p className="font-semibold">
{t("Keywords to include in the text", { defaultValue: "Keywords To Include In The Text" })} {t("Keywords to include in the text", {
defaultValue: "Keywords To Include In The Text",
})}
</p>
<p className="text-sm">
{t("title-key", { defaultValue: "Title Key" })}
</p> </p>
<p className="text-sm">{t("title-key", { defaultValue: "Title Key" })}</p>
<div className="mt-3"> <div className="mt-3">
<Textarea <Textarea
value={selectedSEO} value={selectedSEO}
@ -979,7 +1060,12 @@ export default function FormTeks() {
</div> </div>
</div> </div>
<div className="mt-5"> <div className="mt-5">
<Label>{t("special-instructions", { defaultValue: "Special Instructions" })}(Optional)</Label> <Label>
{t("special-instructions", {
defaultValue: "Special Instructions",
})}
(Optional)
</Label>
<div className="mt-3"> <div className="mt-3">
<Controller <Controller
control={control} control={control}
@ -1045,7 +1131,9 @@ export default function FormTeks() {
</div> </div>
</div> </div>
<div className="py-3 space-y-2"> <div className="py-3 space-y-2">
<Label>{t("description", { defaultValue: "Description" })}</Label> <Label>
{t("description", { defaultValue: "Description" })}
</Label>
<Controller <Controller
control={control} control={control}
name="description" name="description"
@ -1089,7 +1177,9 @@ export default function FormTeks() {
</Label> </Label>
</div> </div>
<div className="py-3 space-y-2"> <div className="py-3 space-y-2">
<Label>{t("description", { defaultValue: "Description" })}</Label> <Label>
{t("description", { defaultValue: "Description" })}
</Label>
<Controller <Controller
control={control} control={control}
name="descriptionOri" name="descriptionOri"
@ -1130,7 +1220,7 @@ export default function FormTeks() {
<Button <Button
type="button" type="button"
key={index} key={index}
className={`mr-3 px-3 py-2 rounded-md ${ className={`mr-3 px-3 py-2 rounded-md cursor-pointer ${
selectedArticleId === id selectedArticleId === id
? "bg-green-500 text-white" ? "bg-green-500 text-white"
: "border-2 border-green-500 bg-white text-green-500 hover:bg-green-500 hover:text-white hover:border-green-500" : "border-2 border-green-500 bg-white text-green-500 hover:bg-green-500 hover:text-white hover:border-green-500"
@ -1149,7 +1239,11 @@ export default function FormTeks() {
</Label> </Label>
</div> </div>
<div className="py-3 space-y-2"> <div className="py-3 space-y-2">
<Label>{t("file-rewrite", { defaultValue: "File Rewrite" })}</Label> <Label>
{t("file-rewrite", {
defaultValue: "File Rewrite",
})}
</Label>
<Controller <Controller
control={control} control={control}
name="rewriteDescription" name="rewriteDescription"
@ -1178,46 +1272,52 @@ export default function FormTeks() {
</> </>
)} )}
<div className="py-3 space-y-2"> <div className="py-3 space-y-2">
<Label>{t("select-file", { defaultValue: "Select File" })}</Label> <Label>
{/* <Input {t("select-file", { defaultValue: "Select File" })}
id="fileInput" </Label>
type="file" <Controller
onChange={handleImageChange} control={control}
/> */} name="files"
<Fragment> render={({ field }) => (
<div {...getRootProps({ className: "dropzone" })}> <>
<input {...getInputProps()} /> <div {...getRootProps({ className: "dropzone" })}>
<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"> <input {...getInputProps()} />
<CloudUpload className="text-default-300 w-10 h-10" /> <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">
<h4 className=" text-2xl font-medium mb-1 mt-3 text-card-foreground/80"> <CloudUpload className="text-default-300 w-10 h-10" />
{/* Drop files here or click to upload. */} <h4 className="text-2xl font-medium mb-1 mt-3 text-card-foreground/80">
{t("drag-file", { defaultValue: "Drag File" })} {t("drag-file", { defaultValue: "Drag File" })}
</h4> </h4>
<div className=" text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
{t("upload-file-text-max", { defaultValue: "Upload File Text Max" })} {t("upload-file-text-max", {
</div> defaultValue: "Upload File Text Max",
</div> })}
</div>
{files.length ? (
<Fragment>
<div>{fileList}</div>
<div className=" flex justify-between gap-2">
{/* <div className="flex flex-row items-center gap-3 py-3">
<Label>{t("watermark", { defaultValue: "Watermark" })}</Label>
<div className="flex items-center gap-3">
<Switch defaultChecked color="primary" id="c2" />
</div> </div>
</div> */} </div>
<Button
color="destructive"
onClick={handleRemoveAllFiles}
>
{t("remove-all", { defaultValue: "Remove All" })}
</Button>
</div> </div>
</Fragment> {files.length ? (
) : null} <>
</Fragment> <div>{fileList}</div>
<div className="flex justify-between gap-2">
<Button
color="destructive"
onClick={() => {
setFiles([]);
field.onChange([]);
}}
>
{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> </div>
@ -1250,36 +1350,51 @@ export default function FormTeks() {
</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">{t("tags", { defaultValue: "Tags" })}</Label> <Label htmlFor="tags">
{t("tags", { defaultValue: "Tags" })}
<Input </Label>
type="text" <Controller
id="tags" control={control}
placeholder="Add a tag and press Enter" name="tags"
onKeyDown={handleAddTag} render={({ field }) => (
ref={inputRef} <>
<Input
type="text"
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>
</>
)}
/> />
<div className="mt-3 "> {errors.tags?.message && (
{tags.map((tag, index) => ( <p className="text-red-400 text-sm">{errors.tags.message}</p>
<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>{t("publish-target", { defaultValue: "Publish Target" })}</Label> <Label>
{t("publish-target", { defaultValue: "Publish Target" })}
</Label>
{options.map((option) => ( {options.map((option) => (
<div key={option.id} className="flex gap-2 items-center"> <div key={option.id} className="flex gap-2 items-center">
<Checkbox <Checkbox

View File

@ -157,22 +157,54 @@ export default function FormVideo() {
{ id: "8", label: "KSP" }, { id: "8", label: "KSP" },
]; ];
const MAX_FILE_SIZE = 100 * 1024 * 1024;
const ACCEPTED_FILE_TYPES = ["video/mp4", "video/quicktime"];
const { getRootProps, getInputProps } = useDropzone({ const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) => {
setFiles(acceptedFiles.map((file) => Object.assign(file)));
},
accept: { accept: {
"video/*": [], "video/mp4": [".mp4"],
"video/quicktime": [".mov"],
},
maxSize: MAX_FILE_SIZE,
onDrop: (acceptedFiles: File[]) => {
const filteredFiles = acceptedFiles.filter(
(file) =>
ACCEPTED_FILE_TYPES.includes(file.type) && file.size <= MAX_FILE_SIZE
);
const filesWithPreview = filteredFiles.map((file) =>
Object.assign(file, {
preview: URL.createObjectURL(file),
})
);
setFiles(filesWithPreview);
setValue("files", filesWithPreview);
}, },
}); });
const videoSchema = z.object({ const videoSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }), title: z.string().min(1, { message: "Judul diperlukan" }),
description: z.string().optional(), description: z.string().optional(),
descriptionOri: z.string().optional(), // Original editor descriptionOri: z.string().optional(),
rewriteDescription: z.string().optional(), rewriteDescription: z.string().optional(),
creatorName: z.string().min(1, { message: "Creator diperlukan" }), creatorName: z.string().min(1, { message: "Creator diperlukan" }),
// tags: z.string().min(1, { message: "Judul diperlukan" }), category: z.string().min(1, { message: "Kategori harus dipilih" }),
tags: z
.array(z.string().min(1))
.min(1, { message: "Minimal 1 tag diperlukan" }),
files: z
.array(z.any())
.min(1, { message: "File video harus dipilih" })
.refine(
(files) =>
files.every((file: File) => ACCEPTED_FILE_TYPES.includes(file.type)),
{ message: "File harus berformat mp4 atau mov" }
)
.refine(
(files) => files.every((file: File) => file.size <= MAX_FILE_SIZE),
{ message: "Ukuran file maksimal 100 MB" }
),
}); });
const { const {
@ -181,12 +213,15 @@ export default function FormVideo() {
getValues, getValues,
setValue, setValue,
formState: { errors }, formState: { errors },
} = useForm<VideoSchema>({ } = useForm<z.infer<typeof videoSchema>>({
resolver: zodResolver(videoSchema), resolver: zodResolver(videoSchema),
defaultValues: { defaultValues: {
description: "", description: "",
descriptionOri: "", descriptionOri: "",
rewriteDescription: "", rewriteDescription: "",
category: "",
files: [],
tags: [],
}, },
}); });
@ -387,16 +422,16 @@ export default function FormVideo() {
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]); // Add new tag setTags((prevTags) => [...prevTags, newTag]);
if (inputRef.current) { if (inputRef.current) {
inputRef.current.value = ""; // Clear input field inputRef.current.value = "";
} }
} }
} }
}; };
const handleRemoveTag = (index: number) => { const handleRemoveTag = (index: number) => {
setTags((prevTags) => prevTags.filter((_, i) => i !== index)); // Remove tag setTags((prevTags) => prevTags.filter((_, i) => i !== index));
}; };
const handleRemoveImage = (index: number) => { const handleRemoveImage = (index: number) => {
@ -428,7 +463,7 @@ export default function FormVideo() {
if (findCategory) { if (findCategory) {
// setValue("categoryId", findCategory.id); // setValue("categoryId", findCategory.id);
setSelectedCategory(findCategory.id); // Set the selected category setSelectedCategory(findCategory.id);
const response = await getTagsBySubCategoryId(findCategory.id); const response = await getTagsBySubCategoryId(findCategory.id);
setTags(response?.data?.data); setTags(response?.data?.data);
} }
@ -441,10 +476,8 @@ export default function FormVideo() {
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")
@ -456,7 +489,6 @@ export default function FormVideo() {
? 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 {
@ -467,7 +499,6 @@ export default function FormVideo() {
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);
} }
@ -503,7 +534,7 @@ export default function FormVideo() {
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,
@ -548,8 +579,6 @@ export default function FormVideo() {
} }
} }
} }
// Upload File
const progressInfoArr = []; const progressInfoArr = [];
for (const item of files) { for (const item of files) {
progressInfoArr.push({ percentage: 0, fileName: item.name }); progressInfoArr.push({ percentage: 0, fileName: item.name });
@ -813,7 +842,9 @@ export default function FormVideo() {
<div className="flex flex-col lg:flex-row gap-10"> <div className="flex flex-col lg:flex-row gap-10">
<Card className="w-full lg:w-8/12"> <Card className="w-full lg:w-8/12">
<div className="px-6 py-6"> <div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">{t("form-video", { defaultValue: "Form Video" })}</p> <p className="text-lg font-semibold mb-3">
{t("form-video", { defaultValue: "Form Video" })}
</p>
<div className="gap-5 mb-5"> <div className="gap-5 mb-5">
<div className="space-y-2 py-3"> <div className="space-y-2 py-3">
<Label>{t("title", { defaultValue: "Title" })}</Label> <Label>{t("title", { defaultValue: "Title" })}</Label>
@ -838,31 +869,45 @@ export default function FormVideo() {
<div className="flex items-center"> <div className="flex items-center">
<div className="py-3 w-full space-y-2"> <div className="py-3 w-full space-y-2">
<Label>{t("category", { defaultValue: "Category" })}</Label> <Label>{t("category", { defaultValue: "Category" })}</Label>
<Select <Controller
value={selectedCategory} // Ensure selectedTarget is updated correctly control={control}
onValueChange={(id) => { name="category"
console.log("Selected Category ID:", id); render={({ field }) => (
setSelectedCategory(id); <Select
}} value={field.value}
> onValueChange={(id) => {
<SelectTrigger size="md"> field.onChange(id);
<SelectValue placeholder="Pilih" /> console.log("Selected Category ID:", id);
</SelectTrigger> setSelectedCategory(id); // tetap set ini kalau mau
<SelectContent> }}
{categories.map((category) => ( >
<SelectItem <SelectTrigger size="md">
key={category.id} <SelectValue placeholder="Pilih" />
value={category.id.toString()} </SelectTrigger>
> <SelectContent>
{category.name} {categories.map((category) => (
</SelectItem> <SelectItem
))} key={category.id}
</SelectContent> value={category.id.toString()}
</Select> >
{category.name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
{errors.category && (
<p className="text-red-500 text-sm">
{errors.category.message}
</p>
)}
</div> </div>
</div> </div>
<div className="flex flex-row items-center gap-3 py-2"> <div className="flex flex-row items-center gap-3 py-2">
<Label>{t("ai-assistance", { defaultValue: "Ai Assistance" })}</Label> <Label>
{t("ai-assistance", { defaultValue: "Ai Assistance" })}
</Label>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Switch <Switch
defaultChecked={isSwitchOn} defaultChecked={isSwitchOn}
@ -878,7 +923,9 @@ export default function FormVideo() {
<div> <div>
<div className="flex flex-row gap-3"> <div className="flex flex-row gap-3">
<div className="space-y-2 py-3 w-4/12"> <div className="space-y-2 py-3 w-4/12">
<Label>{t("language", { defaultValue: "Language" })}</Label> <Label>
{t("language", { defaultValue: "Language" })}
</Label>
<Select onValueChange={setSelectedLanguage}> <Select onValueChange={setSelectedLanguage}>
<SelectTrigger size="md"> <SelectTrigger size="md">
<SelectValue placeholder="Pilih" /> <SelectValue placeholder="Pilih" />
@ -890,7 +937,9 @@ export default function FormVideo() {
</Select> </Select>
</div> </div>
<div className="space-y-2 py-3 w-4/12"> <div className="space-y-2 py-3 w-4/12">
<Label>{t("writing-style", { defaultValue: "Writing Style" })}</Label> <Label>
{t("writing-style", { defaultValue: "Writing Style" })}
</Label>
<Select onValueChange={setSelectedWritingStyle}> <Select onValueChange={setSelectedWritingStyle}>
<SelectTrigger size="md"> <SelectTrigger size="md">
<SelectValue placeholder="Pilih" /> <SelectValue placeholder="Pilih" />
@ -909,7 +958,9 @@ export default function FormVideo() {
</Select> </Select>
</div> </div>
<div className="space-y-2 py-3 w-4/12"> <div className="space-y-2 py-3 w-4/12">
<Label>{t("article-size", { defaultValue: "Article Size" })}</Label> <Label>
{t("article-size", { defaultValue: "Article Size" })}
</Label>
<Select onValueChange={setSelectedSize}> <Select onValueChange={setSelectedSize}>
<SelectTrigger size="md"> <SelectTrigger size="md">
<SelectValue placeholder="Pilih" /> <SelectValue placeholder="Pilih" />
@ -930,7 +981,9 @@ export default function FormVideo() {
</div> </div>
<div className="mt-5"> <div className="mt-5">
<div className="flex flex-row items-center gap-3 mb-3"> <div className="flex flex-row items-center gap-3 mb-3">
<Label>{t("main-keyword", { defaultValue: "Main Keyword" })}</Label> <Label>
{t("main-keyword", { defaultValue: "Main Keyword" })}
</Label>
<Button <Button
variant="outline" variant="outline"
color="primary" color="primary"
@ -987,9 +1040,13 @@ export default function FormVideo() {
</Button> </Button>
</div> </div>
<p className="font-semibold"> <p className="font-semibold">
{t("Keywords to include in the text", { defaultValue: "Keywords To Include In The Text" })} {t("Keywords to include in the text", {
defaultValue: "Keywords To Include In The Text",
})}
</p>
<p className="text-sm">
{t("title-key", { defaultValue: "Title Key" })}
</p> </p>
<p className="text-sm">{t("title-key", { defaultValue: "Title Key" })}</p>
<div className="mt-3"> <div className="mt-3">
<Textarea <Textarea
value={selectedSEO} value={selectedSEO}
@ -999,7 +1056,12 @@ export default function FormVideo() {
</div> </div>
</div> </div>
<div className="mt-5"> <div className="mt-5">
<Label>{t("special-instructions", { defaultValue: "Special Instructions" })}(Optional)</Label> <Label>
{t("special-instructions", {
defaultValue: "Special Instructions",
})}
(Optional)
</Label>
<div className="mt-3"> <div className="mt-3">
<Controller <Controller
control={control} control={control}
@ -1065,7 +1127,9 @@ export default function FormVideo() {
</div> </div>
</div> </div>
<div className="py-3 space-y-2"> <div className="py-3 space-y-2">
<Label>{t("description", { defaultValue: "Description" })}</Label> <Label>
{t("description", { defaultValue: "Description" })}
</Label>
<Controller <Controller
control={control} control={control}
name="description" name="description"
@ -1109,7 +1173,9 @@ export default function FormVideo() {
</Label> </Label>
</div> </div>
<div className="py-3 space-y-2"> <div className="py-3 space-y-2">
<Label>{t("description", { defaultValue: "Description" })}</Label> <Label>
{t("description", { defaultValue: "Description" })}
</Label>
<Controller <Controller
control={control} control={control}
name="descriptionOri" name="descriptionOri"
@ -1169,7 +1235,11 @@ export default function FormVideo() {
</Label> </Label>
</div> </div>
<div className="py-3 space-y-2"> <div className="py-3 space-y-2">
<Label>{t("file-rewrite", { defaultValue: "File Rewrite" })}</Label> <Label>
{t("file-rewrite", {
defaultValue: "File Rewrite",
})}
</Label>
<Controller <Controller
control={control} control={control}
name="rewriteDescription" name="rewriteDescription"
@ -1197,55 +1267,67 @@ export default function FormVideo() {
</RadioGroup> </RadioGroup>
</> </>
)} )}
<div className="py-3 space-y-2"> <Controller
<Label>{t("select-file", { defaultValue: "Select File" })}</Label> control={control}
{/* <Input name="files"
id="fileInput" render={({ field }) => (
type="file" <div className="py-3 space-y-2">
onChange={handleImageChange} <Label>
/> */} {t("select-file", { defaultValue: "Select File" })}
<Fragment> </Label>
<div {...getRootProps({ className: "dropzone" })}> <div {...getRootProps({ className: "dropzone" })}>
<input {...getInputProps()} /> <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"> <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" /> <CloudUpload className="text-default-300 w-10 h-10" />
<h4 className=" text-2xl font-medium mb-1 mt-3 text-card-foreground/80"> <h4 className="text-2xl font-medium mb-1 mt-3 text-card-foreground/80">
{/* Drop files here or click to upload. */} {t("drag-file", { defaultValue: "Drag File" })}
{t("drag-file", { defaultValue: "Drag File" })} </h4>
</h4> <div className="text-xs text-muted-foreground">
<div className=" text-xs text-muted-foreground"> {t("upload-file-video-max", {
{t("upload-file-video-max", { defaultValue: "Upload File Video Max" })} defaultValue: "Upload File Video Max",
})}
</div>
</div> </div>
</div> </div>
</div>
{files.length ? (
<Fragment>
<div>{fileList}</div>
<div className=" flex justify-between gap-2">
{/* <div className="flex flex-row items-center gap-3 py-3">
<Label>{t("watermark", { defaultValue: "Watermark" })}</Label>
<div className="flex items-center gap-3">
<Switch defaultChecked color="primary" id="c2" />
</div>
</div> */}
<Button
color="destructive"
onClick={handleRemoveAllFiles}
>
{t("remove-all", { defaultValue: "Remove All" })}
</Button>
</div>
</Fragment>
) : null}
</Fragment>
</div>
</div>
{/* Submit Button */} {files.length > 0 && (
<div className="mt-2 space-y-1">
{files.map((file, idx) => (
<div
key={idx}
className="flex items-center justify-between rounded border border-default-200 dark:border-default-300 px-2 py-1 text-sm"
>
<span className="truncate">{file.name}</span>
<span className="text-muted-foreground">
{(file.size / (1024 * 1024)).toFixed(2)} MB
</span>
</div>
))}
<div className="flex justify-between gap-2 mt-1">
<Button
color="destructive"
onClick={handleRemoveAllFiles}
>
{t("remove-all", { defaultValue: "Remove All" })}
</Button>
</div>
</div>
)}
{errors.files && (
<p className="text-red-500 text-sm">
{errors.files.message}
</p>
)}
</div>
)}
/>
</div>
</div> </div>
</Card> </Card>
<div className="w-full lg:w-4/12"> <div className="w-full lg:w-4/12">
<Card className=" h-[800px]"> <Card className="h-fit">
<div className="px-3 py-3"> <div className="px-3 py-3">
<div className="space-y-2"> <div className="space-y-2">
<Label>{t("creator", { defaultValue: "Creator" })}</Label> <Label>{t("creator", { defaultValue: "Creator" })}</Label>
@ -1283,36 +1365,67 @@ export default function FormVideo() {
</div> </div>
)} )}
<div className="px-3 py-3 space-y-2"> <div className="px-3 py-3 space-y-2">
<Label htmlFor="tags">{t("tags", { defaultValue: "Tags" })}</Label> <Label htmlFor="tags">
{t("tags", { defaultValue: "Tags" })}
</Label>
<Input <Controller
type="text" control={control}
id="tags" name="tags"
placeholder="Add a tag and press Enter" render={({ field }) => (
onKeyDown={handleAddTag} <>
ref={inputRef} <Input
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>
</>
)}
/> />
<div className="mt-3 ">
{tags.map((tag, index) => ( {/* Tampilkan error */}
<span {errors.tags?.message && (
key={index} <p className="text-red-400 text-sm">{errors.tags.message}</p>
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>{t("publish-target", { defaultValue: "Publish Target" })}</Label> <Label>
{t("publish-target", { defaultValue: "Publish Target" })}
</Label>
{options.map((option) => ( {options.map((option) => (
<div key={option.id} className="flex gap-2 items-center"> <div key={option.id} className="flex gap-2 items-center">
<Checkbox <Checkbox

View File

@ -93,7 +93,7 @@ export default function CreateSettingTracking() {
<Input <Input
size={"md"} size={"md"}
type="number" type="number"
placeholder="Masukan Nama Iklan" placeholder="Masukan Angka"
/> />
</div> </div>
</FormItem> </FormItem>

45
components/wavesurfer.tsx Normal file
View File

@ -0,0 +1,45 @@
// components/WavesurferPlayer.tsx
import React, { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
import WaveSurfer from "wavesurfer.js";
type Props = {
url: string;
height?: number;
waveColor?: string;
};
export type WavesurferPlayerHandle = {
play: () => void;
pause: () => void;
isPlaying: () => boolean;
};
const WavesurferPlayer = forwardRef<WavesurferPlayerHandle, Props>(({ url, height = 100, waveColor = "red" }, ref) => {
const containerRef = useRef<HTMLDivElement>(null);
const waveSurferRef = useRef<WaveSurfer | null>(null);
useEffect(() => {
if (containerRef.current) {
waveSurferRef.current = WaveSurfer.create({
container: containerRef.current,
waveColor,
height,
});
waveSurferRef.current.load(url);
}
return () => {
waveSurferRef.current?.destroy();
};
}, [url, waveColor, height]);
useImperativeHandle(ref, () => ({
play: () => { waveSurferRef.current?.play(); },
pause: () => { waveSurferRef.current?.pause(); },
isPlaying: () => !!waveSurferRef.current?.isPlaying(),
}));
return <div ref={containerRef} className="w-full rounded-md overflow-hidden" />;
});
export default WavesurferPlayer;

View File

@ -747,6 +747,9 @@
"action": "Actions" "action": "Actions"
}, },
"Form": { "Form": {
"titleRequired": "Title required",
"creatorRequired": "Creator required",
"enterTitle": "Enter Title",
"no": "No", "no": "No",
"title": "Title", "title": "Title",
"category-name": "Category Name", "category-name": "Category Name",
@ -813,7 +816,7 @@
"view-file": "View File", "view-file": "View File",
"update": "Update", "update": "Update",
"upload-file-video-max": " Upload files with mp4 or mov Maximum size 100mb.", "upload-file-video-max": " Upload files with mp4 or mov Maximum size 100mb.",
"upload-file-text-max": " Upload files in .doc, .docx, .pdf, .ppt, or .pptx format. Maximum size 100mb.", "upload-file-text-max": " Upload files in .doc, .docx, .pdf, .ppt, or .pptx format. Maximum size 20mb.",
"upload-file-audio-max": " Upload file in mp3 atau wav Maximum size 100mb", "upload-file-audio-max": " Upload file in mp3 atau wav Maximum size 100mb",
"file-rewrite": "File Rewrite", "file-rewrite": "File Rewrite",
"file-placement": "File Placement", "file-placement": "File Placement",
@ -849,6 +852,10 @@
"data-media": "please complete the data! ", "data-media": "please complete the data! ",
"title-media-online": "Online Media Name", "title-media-online": "Online Media Name",
"url": "Url", "url": "Url",
"coverage-area": "Coverage Area" "coverage-area": "Coverage Area",
"only": "Only .jpg, .jpeg, .png files are allowed",
"size": "File too large. Max 100MB",
"onlyVd": "Upload files with mp4 or mov Maximum size 100mb."
} }
} }

View File

@ -748,6 +748,10 @@
"action": "Aksi" "action": "Aksi"
}, },
"Form": { "Form": {
"titleRequired": "Judul diperlukan",
"creatorRequired": "Kreator diperlukan",
"enterTitle": "Masukkan Judul",
"no": "Nomor", "no": "Nomor",
"title": "Judul", "title": "Judul",
"category-name": "Nama Kategori", "category-name": "Nama Kategori",
@ -789,7 +793,7 @@
"main-keyword": "Main Keyword", "main-keyword": "Main Keyword",
"seo": "Seo", "seo": "Seo",
"Keywords to include in the text": "Kata kunci untuk di sertakan dalam teks", "Keywords to include in the text": "Kata kunci untuk di sertakan dalam teks",
"title-key": "JIka Anda tidak Memberikan kata kunci, kami akan secara otomatis membuat kata kunci yang relevan dari kata kunciutama untuk setiap bagian dan menggunakannya untuk membuat artikel. Untuk menambahkan kata kunci baru, ketik kata kunci", "title-key": "JIka Anda tidak Memberikan kata kunci, kami akan secara otomatis membuat kata kunci yang relevan dari kata kunci utama untuk setiap bagian dan menggunakannya untuk membuat artikel. Untuk menambahkan kata kunci baru, ketik kata kunci",
"special-instructions": "Instruksi Khusus", "special-instructions": "Instruksi Khusus",
"description": "Deskripsi", "description": "Deskripsi",
"select-file": "Pilih File", "select-file": "Pilih File",
@ -814,7 +818,7 @@
"view-file": "Lihat file", "view-file": "Lihat file",
"update": "Edit", "update": "Edit",
"upload-file-video-max": " Upload file dengan mp4 atau mov Ukuran maksimal 100mb.", "upload-file-video-max": " Upload file dengan mp4 atau mov Ukuran maksimal 100mb.",
"upload-file-text-max": "Upload file dengan format .doc, .docx, .pdf, .ppt, atau .pptx Ukuran maksimal 100mb. ", "upload-file-text-max": "Upload file dengan format .doc, .docx, .pdf, .ppt, atau .pptx Ukuran maksimal 20mb. ",
"upload-file-audio-max": " Upload file dengan mp3 atau wav maksimal ukuran 100mb", "upload-file-audio-max": " Upload file dengan mp3 atau wav maksimal ukuran 100mb",
"file-rewrite": "File hasil Rewrite", "file-rewrite": "File hasil Rewrite",
"file-placement": "Penempatan file", "file-placement": "Penempatan file",
@ -849,6 +853,9 @@
"data-media": "Silahkan Lengkapi Data!", "data-media": "Silahkan Lengkapi Data!",
"title-media-online": "Nama Media Online", "title-media-online": "Nama Media Online",
"url": "Url", "url": "Url",
"coverage-area": "Cakupan Wilayah" "coverage-area": "Cakupan Wilayah",
"only": "Hanya file .jpg, .jpeg, .png yang diizinkan",
"size": "File terlalu besar. Maksimal 100MB",
"onlyVd": "Upload file dengan mp4 atau mov Ukuran maksimal 100mb."
} }
} }