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
</Button>
</Link>
<Link href="/admin/broadcast/campaign-list/import">
{/* <Link href="/admin/broadcast/campaign-list/import">
<Button color="success" size="md" className="text-sm">
<UserIcon />
Import Akun
</Button>
</Link>
</Link> */}
</div>
</div>
<div className="flex justify-end">

View File

@ -44,7 +44,7 @@ const FormSchema = z.object({
.refine((value) => value.some((item) => item), {
message: "Required",
}),
accountCategory: z.enum(["polri", "jurnalis", "umumu", "ksp"], {
accountCategory: z.enum(["polri", "jurnalis", "umum", "ksp"], {
required_error: "Required",
}),
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 { formatDateToIndonesian } from "@/utils/globals";
import ApprovalHistoryModal from "@/components/modal/approval-history-modal";
import { useDropzone } from "react-dropzone";
import AudioPlayer from "@/components/audio-player";
const imageSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }),
@ -139,32 +141,54 @@ export default function FormAudioDetail() {
const [selectedPublishers, setSelectedPublishers] = useState<number[]>([]);
const [description, setDescription] = useState("");
const [main, setMain] = useState<any>([]);
const [detailThumb, setDetailThumb] = useState<any>([]);
const [thumbsSwiper, setThumbsSwiper] = useState<any>(null);
const t = useTranslations("Form");
const [selectedTarget, setSelectedTarget] = useState("");
const [files, setFiles] = useState<FileType[]>([]);
const [rejectedFiles, setRejectedFiles] = useState<number[]>([]);
const [isUserMabesApprover, setIsUserMabesApprover] = useState(false);
const [audioPlaying, setAudioPlaying] = useState<any>(null);
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 waveSurfersRef = useRef<WaveSurfer[]>([]);
const [isPlayingIndex, setIsPlayingIndex] = useState<number | null>(null);
const [wavesurfer, setWavesurfer] = useState<WaveSurfer>();
const [isPlaying, setIsPlaying] = useState(false);
const [approval, setApproval] = useState<any>();
const onReady = (ws: any) => {
setWavesurfer(ws);
setIsPlaying(false);
const onDrop = (acceptedFiles: File[]) => {
setUploadedFiles(acceptedFiles);
const blobUrls = acceptedFiles.map((file) => URL.createObjectURL(file));
setDetailThumb(blobUrls);
};
const onPlayPause = () => {
wavesurfer && wavesurfer.playPause();
const onReady = (ws: WaveSurfer, index: number) => {
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";
const {
@ -261,8 +285,8 @@ export default function FormAudioDetail() {
});
setupPlacementCheck(details?.files?.length);
if (details.publishedForObject) {
const publisherIds = details.publishedForObject.map(
if (details?.publishedForObject) {
const publisherIds = details?.publishedForObject.map(
(obj: any) => obj.id
);
setSelectedPublishers(publisherIds);
@ -276,9 +300,9 @@ export default function FormAudioDetail() {
setSelectedTarget(matchingCategory.name);
}
setSelectedTarget(details.categoryId); // Untuk dropdown
setSelectedTarget(details?.categoryId);
const filesData = details.files || [];
const filesData = details?.files || [];
const audioFiles = filesData.filter(
(file: any) =>
file.contentType && file.contentType.startsWith("video/webm")
@ -439,9 +463,9 @@ export default function FormAudioDetail() {
const handleAudioPlayPause = (audioSrc: string) => {
if (audioPlaying === audioSrc) {
setAudioPlaying(null); // Pause if the same audio is clicked
setAudioPlaying(null);
} 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">
<Card className="w-full lg:w-8/12">
<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">
{/* Input Title */}
<div className="space-y-2 py-3">
@ -491,7 +517,7 @@ export default function FormAudioDetail() {
<div className="py-3 w-full space-y-2">
<Label>{t("category", { defaultValue: "Category" })}</Label>
<Select
value={detail?.category.name} // Nilai default berdasarkan detail
value={detail?.category.name}
onValueChange={(id) => {
console.log("Selected Category:", id);
setSelectedTarget(id);
@ -512,7 +538,9 @@ export default function FormAudioDetail() {
</div>
<div className="py-3 space-y-2">
<Label>{t("description", { defaultValue: "Description" })}</Label>
<Label>
{t("description", { defaultValue: "Description" })}
</Label>
<Controller
control={control}
name="description"
@ -527,42 +555,25 @@ export default function FormAudioDetail() {
)}
</div>
<Label className="text-xl space-y-2">{t("file-media", { defaultValue: "File Media" })}</Label>
<div className="w-full">
<div className={"container example"}>
{detailThumb?.map((url: any, index: number) => (
<div key={url.id}>
<WavesurferPlayer
height={500}
waveColor="red"
<Label className="text-xl space-y-2">
{t("file-media", { defaultValue: "File Media" })}
</Label>
<div className="container example">
{detailThumb.map((url, index) => (
<div key={index}>
{/* <WavesurferPlayer
url={url}
onReady={onReady}
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
waveColor="red"
height={80}
onReady={(ws: any) => onReady(ws, index)}
/>
<Button onClick={() => onPlayPause(index)}>
{isPlayingIndex === index ? "Pause" : "Play"}
</Button> */}
<AudioPlayer urlAudio={url} />
</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>
@ -613,7 +624,9 @@ export default function FormAudioDetail() {
</div>
<div className="px-3 py-3">
<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">
<Checkbox
id="5"
@ -675,7 +688,9 @@ export default function FormAudioDetail() {
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
<DialogContent size="md">
<DialogHeader>
<DialogTitle>{t("leave-comment", { defaultValue: "Leave Comment" })}</DialogTitle>
<DialogTitle>
{t("leave-comment", { defaultValue: "Leave Comment" })}
</DialogTitle>
</DialogHeader>
{status == "2"
? files?.map((file, index) => (
@ -893,7 +908,8 @@ export default function FormAudioDetail() {
color="primary"
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
onClick={() => actionApproval("3")}

View File

@ -156,20 +156,33 @@ export default function FormAudio() {
{ id: "8", label: "KSP" },
];
const audioRefs = useRef<HTMLAudioElement[]>([]);
const { getRootProps, getInputProps } = useDropzone({
multiple: true,
accept: { "audio/*": [] },
onDrop: (acceptedFiles) => {
setFiles(acceptedFiles.map((file) => Object.assign(file)));
},
accept: {
"audio/*": [],
const filesWithPreview = acceptedFiles.map((file) =>
Object.assign(file, { preview: URL.createObjectURL(file) })
);
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({
title: z.string().min(1, { message: "Judul diperlukan" }),
description: z.string().optional(),
descriptionOri: z.string().optional(), // Original editor
rewriteDescription: z.string().optional(), // Rewrite editor
descriptionOri: z.string().optional(),
rewriteDescription: z.string().optional(),
creatorName: z.string().min(1, { message: "Creator 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">
<Card className="w-full lg:w-8/12">
<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">
{/* Input Title */}
<div className="space-y-2 py-3">
@ -830,7 +845,7 @@ export default function FormAudio() {
<div className="py-3 w-full space-y-2">
<Label>{t("category", { defaultValue: "Category" })}</Label>
<Select
value={selectedCategory} // Ensure selectedTarget is updated correctly
value={selectedCategory}
onValueChange={(id) => {
console.log("Selected Category ID:", id);
setSelectedCategory(id);
@ -853,7 +868,9 @@ export default function FormAudio() {
</div>
</div>
<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">
<Switch
defaultChecked={isSwitchOn}
@ -869,7 +886,9 @@ export default function FormAudio() {
<div>
<div className="flex flex-row gap-3">
<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}>
<SelectTrigger size="md">
<SelectValue placeholder="Pilih" />
@ -881,7 +900,9 @@ export default function FormAudio() {
</Select>
</div>
<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}>
<SelectTrigger size="md">
<SelectValue placeholder="Pilih" />
@ -900,7 +921,9 @@ export default function FormAudio() {
</Select>
</div>
<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}>
<SelectTrigger size="md">
<SelectValue placeholder="Pilih" />
@ -921,7 +944,9 @@ export default function FormAudio() {
</div>
<div className="mt-5">
<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
variant="outline"
color="primary"
@ -978,9 +1003,13 @@ export default function FormAudio() {
</Button>
</div>
<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 className="text-sm">{t("title-key", { defaultValue: "Title Key" })}</p>
<div className="mt-3">
<Textarea
value={selectedSEO}
@ -990,7 +1019,12 @@ export default function FormAudio() {
</div>
</div>
<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">
<Controller
control={control}
@ -1056,7 +1090,9 @@ export default function FormAudio() {
</div>
</div>
<div className="py-3 space-y-2">
<Label>{t("description", { defaultValue: "Description" })}</Label>
<Label>
{t("description", { defaultValue: "Description" })}
</Label>
<Controller
control={control}
name="description"
@ -1100,7 +1136,9 @@ export default function FormAudio() {
</Label>
</div>
<div className="py-3 space-y-2">
<Label>{t("description", { defaultValue: "Description" })}</Label>
<Label>
{t("description", { defaultValue: "Description" })}
</Label>
<Controller
control={control}
name="descriptionOri"
@ -1160,7 +1198,11 @@ export default function FormAudio() {
</Label>
</div>
<div className="py-3 space-y-2">
<Label>{t("file-rewrite", { defaultValue: "File Rewrite" })}</Label>
<Label>
{t("file-rewrite", {
defaultValue: "File Rewrite",
})}
</Label>
<Controller
control={control}
name="rewriteDescription"
@ -1189,36 +1231,51 @@ export default function FormAudio() {
</>
)}
<div className="py-3 space-y-2">
<Label>{t("select-file", { defaultValue: "Select File" })}</Label>
{/* <Input
id="fileInput"
type="file"
onChange={handleImageChange}
/> */}
<Label>
{t("select-file", { defaultValue: "Select File" })}
</Label>
<Fragment>
<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">
<CloudUpload className="text-default-300 w-10 h-10" />
<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" })}
</h4>
<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>
{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>Gunakan Watermark</Label>
<div className="flex items-center gap-3">
<Switch defaultChecked color="primary" id="c2" />
<div className="space-y-4 mt-4">
{files.map((file, idx) => (
<div
key={idx}
className="flex flex-col gap-2 border p-2 rounded-md"
>
<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 className="flex justify-between gap-2 mt-3">
<Button
color="destructive"
onClick={handleRemoveAllFiles}
@ -1259,7 +1316,9 @@ export default function FormAudio() {
</div>
</div>
<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
type="text"
@ -1288,7 +1347,9 @@ export default function FormAudio() {
</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>
<Label>
{t("publish-target", { defaultValue: "Publish Target" })}
</Label>
{options.map((option) => (
<div key={option.id} className="flex gap-2 items-center">
<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>;
const params = useParams();
const locale = params?.locale;
const t = useTranslations("Form");
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const taskId = Cookies.get("taskId");
@ -171,11 +170,17 @@ export default function FormImage() {
});
const imageSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }),
title: z.string().min(1, { message: t("titleRequired") }),
description: z.string().optional(),
descriptionOri: 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 {
@ -183,6 +188,7 @@ export default function FormImage() {
handleSubmit,
getValues,
setValue,
watch,
formState: { errors },
} = useForm<ImageSchema>({
resolver: zodResolver(imageSchema),
@ -190,6 +196,12 @@ export default function FormImage() {
description: "",
descriptionOri: "",
rewriteDescription: "",
title: "",
creatorName: "",
categoryId: "",
tags: [],
publishedFor: [],
files: [],
},
});
@ -785,7 +797,9 @@ export default function FormImage() {
<div className="flex flex-col lg:flex-row gap-10">
<Card className="w-full lg:w-8/12">
<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">
{/* Input Title */}
<div className="space-y-2 py-3">
@ -799,7 +813,9 @@ export default function FormImage() {
type="text"
value={field.value}
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="py-3 space-y-2 w-full">
<Label>{t("category", { defaultValue: "Category" })}</Label>
<Select
{/* <Select
value={selectedCategory}
onValueChange={(id) => {
console.log("Selected Category ID:", id);
@ -831,11 +847,39 @@ export default function FormImage() {
</SelectItem>
))}
</SelectContent>
</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 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">
<Switch
defaultChecked={isSwitchOn}
@ -851,7 +895,9 @@ export default function FormImage() {
<div>
<div className="flex flex-row gap-3">
<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}>
<SelectTrigger size="md">
<SelectValue placeholder="Pilih" />
@ -863,7 +909,9 @@ export default function FormImage() {
</Select>
</div>
<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}>
<SelectTrigger size="md">
<SelectValue placeholder="Pilih" />
@ -882,7 +930,9 @@ export default function FormImage() {
</Select>
</div>
<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}>
<SelectTrigger size="md">
<SelectValue placeholder="Pilih" />
@ -903,7 +953,9 @@ export default function FormImage() {
</div>
<div className="mt-5">
<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
variant="outline"
color="primary"
@ -960,9 +1012,13 @@ export default function FormImage() {
</Button>
</div>
<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 className="text-sm">{t("title-key", { defaultValue: "Title Key" })}</p>
<div className="mt-3">
<Textarea
value={selectedSEO}
@ -972,7 +1028,12 @@ export default function FormImage() {
</div>
</div>
<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">
<Controller
control={control}
@ -1005,7 +1066,7 @@ export default function FormImage() {
{articleIds.map((id: string, index: number) => (
<p
key={index}
className={`mr-3 px-3 py-2 rounded-md ${
className={`mr-3 px-3 py-2 rounded-md cursor-pointer ${
selectedArticleId === id
? "bg-green-500 text-white"
: "border-2 border-green-500 text-green-500"
@ -1038,7 +1099,9 @@ export default function FormImage() {
</div>
</div>
<div className="py-3 space-y-2">
<Label>{t("description", { defaultValue: "Description" })}</Label>
<Label>
{t("description", { defaultValue: "Description" })}
</Label>
<Controller
control={control}
name="description"
@ -1082,7 +1145,9 @@ export default function FormImage() {
</Label>
</div>
<div className="py-3 space-y-2">
<Label>{t("description", { defaultValue: "Description" })}</Label>
<Label>
{t("description", { defaultValue: "Description" })}
</Label>
<Controller
control={control}
name="descriptionOri"
@ -1142,7 +1207,11 @@ export default function FormImage() {
</Label>
</div>
<div className="py-3 space-y-2">
<Label>{t("file-rewrite", { defaultValue: "File Rewrite" })}</Label>
<Label>
{t("file-rewrite", {
defaultValue: "File Rewrite",
})}
</Label>
<Controller
control={control}
name="rewriteDescription"
@ -1170,37 +1239,141 @@ export default function FormImage() {
</RadioGroup>
</>
)}
<div>
<Controller
control={control}
name="files"
render={({ field }) => {
const maxSize = 100 * 1024 * 1024;
const { getRootProps, getInputProps, fileRejections } =
useDropzone({
accept: {
"image/jpeg": [".jpeg", ".jpg"],
"image/png": [".png"],
},
maxSize,
multiple: false,
onDrop: (acceptedFiles) => {
field.onChange(acceptedFiles);
},
});
return (
<div className="py-3 space-y-2">
<Label>{t("select-file", { defaultValue: "Select File" })}</Label>
{/* <Input
id="fileInput"
type="file"
onChange={handleImageChange}
/> */}
<Fragment>
<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">
{/* Drop files here or click to upload. */}
{t("drag-file", { defaultValue: "Drag File" })}
</h4>
<div className="text-xs text-muted-foreground">
{t("upload-file-max", { defaultValue: "Upload File Max" })}
{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>
{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}
@ -1208,15 +1381,21 @@ export default function FormImage() {
{t("remove-all", { defaultValue: "Remove All" })}
</Button>
</div>
</Fragment>
</>
) : null}
</Fragment>
</div>
</div>
{/* Submit Button */}
{errors.files?.message && (
<p className="text-red-400 text-sm">
{errors.files.message}
</p>
)}
</div>
)}
/> */}
</div>
</div>
</Card>
<div className="w-full lg:w-4/12">
<Card className=" h-[500px]">
<div className="px-3 py-3">
@ -1242,8 +1421,10 @@ export default function FormImage() {
)}
</div>
</div>
<div className="px-3 py-3 space-y-2">
<Label htmlFor="tags">{t("tags", { defaultValue: "Tags" })}</Label>
{/* <div className="px-3 py-3 space-y-2">
<Label htmlFor="tags">
{t("tags", { defaultValue: "Tags" })}
</Label>
<Input
type="text"
id="tags"
@ -1268,10 +1449,70 @@ export default function FormImage() {
</span>
))}
</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>
<div className="px-3 py-3">
</>
)}
/>
{/* Tampilkan error */}
{errors.tags?.message && (
<p className="text-red-400 text-sm">{errors.tags.message}</p>
)}
</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>
<Label>
{t("publish-target", { defaultValue: "Publish Target" })}
</Label>
{options.map((option) => (
<div key={option.id} className="flex gap-2 items-center">
<Checkbox
@ -1289,6 +1530,62 @@ export default function FormImage() {
</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>
</Card>
<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 { formatDateToIndonesian } from "@/utils/globals";
import ApprovalHistoryModal from "@/components/modal/approval-history-modal";
import FileTextPreview from "./file-preview-text";
import FileTextThumbnail from "./file-text-thumbnail";
const imageSchema = z.object({
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">
<Card className="w-full lg:w-8/12">
<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">
{/* Input Title */}
<div className="space-y-2 py-3">
@ -484,7 +488,9 @@ export default function FormTeksDetail() {
</div>
<div className="py-3 space-y-2">
<Label>{t("description", { defaultValue: "Description" })}</Label>
<Label>
{t("description", { defaultValue: "Description" })}
</Label>
<Controller
control={control}
name="description"
@ -499,7 +505,9 @@ export default function FormTeksDetail() {
)}
</div>
<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">
<Swiper
thumbs={{ swiper: thumbsSwiper }}
@ -507,49 +515,13 @@ export default function FormTeksDetail() {
navigation={false}
className="w-full"
>
{detailThumb?.map((data: any, index: number) => (
{detailThumb?.map((file: any, index: any) => (
<SwiperSlide key={index}>
{[".jpg", ".jpeg", ".png", ".webp"].includes(
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>
)}
<FileTextPreview file={file} />
</SwiperSlide>
))}
</Swiper>
<div className="mt-2">
<Swiper
onSwiper={setThumbsSwiper}
@ -558,21 +530,9 @@ export default function FormTeksDetail() {
pagination={{ clickable: true }}
modules={[Pagination, Thumbs]}
>
{detailThumb?.map((data: any, index: number) => (
{detailThumb?.map((file: any, index: any) => (
<SwiperSlide key={index}>
{[".jpg", ".jpeg", ".png", ".webp"].includes(
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>
)}
<FileTextThumbnail file={file} />
</SwiperSlide>
))}
</Swiper>
@ -636,7 +596,9 @@ export default function FormTeksDetail() {
</div>
<div className="px-3 py-3">
<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">
<Checkbox
id="5"
@ -698,7 +660,9 @@ export default function FormTeksDetail() {
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
<DialogContent size="md">
<DialogHeader>
<DialogTitle>{t("leave-comment", { defaultValue: "Leave Comment" })}</DialogTitle>
<DialogTitle>
{t("leave-comment", { defaultValue: "Leave Comment" })}
</DialogTitle>
</DialogHeader>
{status == "2"
? files?.map((file, index) => (
@ -916,7 +880,8 @@ export default function FormTeksDetail() {
color="primary"
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
onClick={() => actionApproval("3")}

View File

@ -154,24 +154,72 @@ export default function FormTeks() {
];
const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) => {
setFiles(acceptedFiles.map((file) => Object.assign(file)));
},
accept: {
"application/pdf": [],
"application/msword": [], // .doc
"application/msword": [],
"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({
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" }),
// 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 {
@ -183,6 +231,11 @@ export default function FormTeks() {
} = useForm<TeksSchema>({
resolver: zodResolver(teksSchema),
defaultValues: {
title: "",
creatorName: "",
category: "",
tags: [],
files: [],
description: "",
descriptionOri: "",
rewriteDescription: "",
@ -381,21 +434,22 @@ export default function FormTeks() {
}
};
const handleAddTag = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && e.currentTarget.value.trim()) {
const handleAddTag = (
e: React.KeyboardEvent<HTMLInputElement>,
field: any
) => {
if (e.key === "Enter") {
e.preventDefault();
const newTag = e.currentTarget.value.trim();
if (!tags.includes(newTag)) {
setTags((prevTags) => [...prevTags, newTag]); // Add new tag
if (inputRef.current) {
inputRef.current.value = ""; // Clear input field
}
const value = e.currentTarget.value.trim();
if (value && !field.value.includes(value)) {
field.onChange([...field.value, value]);
e.currentTarget.value = "";
}
}
};
const handleRemoveTag = (index: number) => {
setTags((prevTags) => prevTags.filter((_, i) => i !== index)); // Remove tag
const handleRemoveTag = (index: number, field: any) => {
const newTags = field.value.filter((_: any, i: number) => i !== index);
field.onChange(newTags);
};
const handleRemoveImage = (index: number) => {
@ -792,7 +846,9 @@ export default function FormTeks() {
<div className="flex flex-col lg:flex-row gap-10">
<Card className="w-full lg:w-8/12">
<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">
{/* Input Title */}
<div className="space-y-2 py-3">
@ -818,11 +874,15 @@ export default function FormTeks() {
<div className="flex items-center">
<div className="py-3 w-full space-y-2">
<Label>{t("category", { defaultValue: "Category" })}</Label>
<Controller
control={control}
name="category"
render={({ field }) => (
<Select
value={selectedCategory} // Ensure selectedTarget is updated correctly
onValueChange={(id) => {
console.log("Selected Category ID:", id);
setSelectedCategory(id);
value={field.value}
onValueChange={(val) => {
field.onChange(val);
setSelectedCategory(val);
}}
>
<SelectTrigger size="md">
@ -839,10 +899,19 @@ export default function FormTeks() {
))}
</SelectContent>
</Select>
)}
/>
{errors.category?.message && (
<p className="text-red-400 text-sm">
{errors.category.message}
</p>
)}
</div>
</div>
<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">
<Switch
defaultChecked={isSwitchOn}
@ -858,7 +927,9 @@ export default function FormTeks() {
<div>
<div className="flex flex-row gap-3">
<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}>
<SelectTrigger size="md">
<SelectValue placeholder="Pilih" />
@ -870,7 +941,9 @@ export default function FormTeks() {
</Select>
</div>
<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}>
<SelectTrigger size="md">
<SelectValue placeholder="Pilih" />
@ -889,7 +962,9 @@ export default function FormTeks() {
</Select>
</div>
<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}>
<SelectTrigger size="md">
<SelectValue placeholder="Pilih" />
@ -910,7 +985,9 @@ export default function FormTeks() {
</div>
<div className="mt-5">
<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
variant="outline"
color="primary"
@ -967,9 +1044,13 @@ export default function FormTeks() {
</Button>
</div>
<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 className="text-sm">{t("title-key", { defaultValue: "Title Key" })}</p>
<div className="mt-3">
<Textarea
value={selectedSEO}
@ -979,7 +1060,12 @@ export default function FormTeks() {
</div>
</div>
<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">
<Controller
control={control}
@ -1045,7 +1131,9 @@ export default function FormTeks() {
</div>
</div>
<div className="py-3 space-y-2">
<Label>{t("description", { defaultValue: "Description" })}</Label>
<Label>
{t("description", { defaultValue: "Description" })}
</Label>
<Controller
control={control}
name="description"
@ -1089,7 +1177,9 @@ export default function FormTeks() {
</Label>
</div>
<div className="py-3 space-y-2">
<Label>{t("description", { defaultValue: "Description" })}</Label>
<Label>
{t("description", { defaultValue: "Description" })}
</Label>
<Controller
control={control}
name="descriptionOri"
@ -1130,7 +1220,7 @@ export default function FormTeks() {
<Button
type="button"
key={index}
className={`mr-3 px-3 py-2 rounded-md ${
className={`mr-3 px-3 py-2 rounded-md cursor-pointer ${
selectedArticleId === id
? "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"
@ -1149,7 +1239,11 @@ export default function FormTeks() {
</Label>
</div>
<div className="py-3 space-y-2">
<Label>{t("file-rewrite", { defaultValue: "File Rewrite" })}</Label>
<Label>
{t("file-rewrite", {
defaultValue: "File Rewrite",
})}
</Label>
<Controller
control={control}
name="rewriteDescription"
@ -1178,46 +1272,52 @@ export default function FormTeks() {
</>
)}
<div className="py-3 space-y-2">
<Label>{t("select-file", { defaultValue: "Select File" })}</Label>
{/* <Input
id="fileInput"
type="file"
onChange={handleImageChange}
/> */}
<Fragment>
<Label>
{t("select-file", { defaultValue: "Select File" })}
</Label>
<Controller
control={control}
name="files"
render={({ field }) => (
<>
<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">
{/* Drop files here or click to upload. */}
{t("drag-file", { defaultValue: "Drag File" })}
</h4>
<div className="text-xs text-muted-foreground">
{t("upload-file-text-max", { defaultValue: "Upload File Text Max" })}
{t("upload-file-text-max", {
defaultValue: "Upload File Text Max",
})}
</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}
onClick={() => {
setFiles([]);
field.onChange([]);
}}
>
{t("remove-all", { defaultValue: "Remove All" })}
</Button>
</div>
</Fragment>
</>
) : null}
</Fragment>
{errors.files?.message && (
<p className="text-red-400 text-sm">
{errors.files.message}
</p>
)}
</>
)}
/>
</div>
</div>
@ -1250,17 +1350,23 @@ export default function FormTeks() {
</div>
</div>
<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>
<Controller
control={control}
name="tags"
render={({ field }) => (
<>
<Input
type="text"
id="tags"
placeholder="Add a tag and press Enter"
onKeyDown={handleAddTag}
onKeyDown={(e) => handleAddTag(e, field)} // pass field ke fungsi
ref={inputRef}
/>
<div className="mt-3">
{tags.map((tag, index) => (
{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"
@ -1268,7 +1374,7 @@ export default function FormTeks() {
{tag}{" "}
<button
type="button"
onClick={() => handleRemoveTag(index)}
onClick={() => handleRemoveTag(index, field)}
className="remove-tag-button"
>
×
@ -1276,10 +1382,19 @@ export default function FormTeks() {
</span>
))}
</div>
</>
)}
/>
{errors.tags?.message && (
<p className="text-red-400 text-sm">{errors.tags.message}</p>
)}
</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>
<Label>
{t("publish-target", { defaultValue: "Publish Target" })}
</Label>
{options.map((option) => (
<div key={option.id} className="flex gap-2 items-center">
<Checkbox

View File

@ -157,22 +157,54 @@ export default function FormVideo() {
{ id: "8", label: "KSP" },
];
const MAX_FILE_SIZE = 100 * 1024 * 1024;
const ACCEPTED_FILE_TYPES = ["video/mp4", "video/quicktime"];
const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) => {
setFiles(acceptedFiles.map((file) => Object.assign(file)));
},
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({
title: z.string().min(1, { message: "Judul diperlukan" }),
description: z.string().optional(),
descriptionOri: z.string().optional(), // Original editor
descriptionOri: z.string().optional(),
rewriteDescription: z.string().optional(),
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 {
@ -181,12 +213,15 @@ export default function FormVideo() {
getValues,
setValue,
formState: { errors },
} = useForm<VideoSchema>({
} = useForm<z.infer<typeof videoSchema>>({
resolver: zodResolver(videoSchema),
defaultValues: {
description: "",
descriptionOri: "",
rewriteDescription: "",
category: "",
files: [],
tags: [],
},
});
@ -387,16 +422,16 @@ export default function FormVideo() {
e.preventDefault();
const newTag = e.currentTarget.value.trim();
if (!tags.includes(newTag)) {
setTags((prevTags) => [...prevTags, newTag]); // Add new tag
setTags((prevTags) => [...prevTags, newTag]);
if (inputRef.current) {
inputRef.current.value = ""; // Clear input field
inputRef.current.value = "";
}
}
}
};
const handleRemoveTag = (index: number) => {
setTags((prevTags) => prevTags.filter((_, i) => i !== index)); // Remove tag
setTags((prevTags) => prevTags.filter((_, i) => i !== index));
};
const handleRemoveImage = (index: number) => {
@ -428,7 +463,7 @@ export default function FormVideo() {
if (findCategory) {
// setValue("categoryId", findCategory.id);
setSelectedCategory(findCategory.id); // Set the selected category
setSelectedCategory(findCategory.id);
const response = await getTagsBySubCategoryId(findCategory.id);
setTags(response?.data?.data);
}
@ -441,10 +476,8 @@ export default function FormVideo() {
const handleCheckboxChange = (id: string): void => {
if (id === "all") {
if (publishedFor.includes("all")) {
// Uncheck all checkboxes
setPublishedFor([]);
} else {
// Select all checkboxes
setPublishedFor(
options
.filter((opt: any) => opt.id !== "all")
@ -456,7 +489,6 @@ export default function FormVideo() {
? publishedFor.filter((item) => item !== id)
: [...publishedFor, id];
// Remove "all" if any checkbox is unchecked
if (publishedFor.includes("all") && id !== "all") {
setPublishedFor(updatedPublishedFor.filter((item) => item !== "all"));
} else {
@ -467,7 +499,6 @@ export default function FormVideo() {
useEffect(() => {
if (articleBody) {
// Set ke dua field jika rewrite juga aktif
setValue("description", articleBody);
setValue("rewriteDescription", articleBody);
}
@ -503,7 +534,7 @@ export default function FormVideo() {
tags: string;
isYoutube: boolean;
isInternationalMedia: boolean;
attachFromScheduleId?: number; // ✅ Tambahkan properti ini
attachFromScheduleId?: number;
} = {
...data,
title: finalTitle,
@ -548,8 +579,6 @@ export default function FormVideo() {
}
}
}
// Upload File
const progressInfoArr = [];
for (const item of files) {
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">
<Card className="w-full lg:w-8/12">
<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="space-y-2 py-3">
<Label>{t("title", { defaultValue: "Title" })}</Label>
@ -838,11 +869,16 @@ export default function FormVideo() {
<div className="flex items-center">
<div className="py-3 w-full space-y-2">
<Label>{t("category", { defaultValue: "Category" })}</Label>
<Controller
control={control}
name="category"
render={({ field }) => (
<Select
value={selectedCategory} // Ensure selectedTarget is updated correctly
value={field.value}
onValueChange={(id) => {
field.onChange(id);
console.log("Selected Category ID:", id);
setSelectedCategory(id);
setSelectedCategory(id); // tetap set ini kalau mau
}}
>
<SelectTrigger size="md">
@ -859,10 +895,19 @@ export default function FormVideo() {
))}
</SelectContent>
</Select>
)}
/>
{errors.category && (
<p className="text-red-500 text-sm">
{errors.category.message}
</p>
)}
</div>
</div>
<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">
<Switch
defaultChecked={isSwitchOn}
@ -878,7 +923,9 @@ export default function FormVideo() {
<div>
<div className="flex flex-row gap-3">
<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}>
<SelectTrigger size="md">
<SelectValue placeholder="Pilih" />
@ -890,7 +937,9 @@ export default function FormVideo() {
</Select>
</div>
<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}>
<SelectTrigger size="md">
<SelectValue placeholder="Pilih" />
@ -909,7 +958,9 @@ export default function FormVideo() {
</Select>
</div>
<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}>
<SelectTrigger size="md">
<SelectValue placeholder="Pilih" />
@ -930,7 +981,9 @@ export default function FormVideo() {
</div>
<div className="mt-5">
<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
variant="outline"
color="primary"
@ -987,9 +1040,13 @@ export default function FormVideo() {
</Button>
</div>
<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 className="text-sm">{t("title-key", { defaultValue: "Title Key" })}</p>
<div className="mt-3">
<Textarea
value={selectedSEO}
@ -999,7 +1056,12 @@ export default function FormVideo() {
</div>
</div>
<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">
<Controller
control={control}
@ -1065,7 +1127,9 @@ export default function FormVideo() {
</div>
</div>
<div className="py-3 space-y-2">
<Label>{t("description", { defaultValue: "Description" })}</Label>
<Label>
{t("description", { defaultValue: "Description" })}
</Label>
<Controller
control={control}
name="description"
@ -1109,7 +1173,9 @@ export default function FormVideo() {
</Label>
</div>
<div className="py-3 space-y-2">
<Label>{t("description", { defaultValue: "Description" })}</Label>
<Label>
{t("description", { defaultValue: "Description" })}
</Label>
<Controller
control={control}
name="descriptionOri"
@ -1169,7 +1235,11 @@ export default function FormVideo() {
</Label>
</div>
<div className="py-3 space-y-2">
<Label>{t("file-rewrite", { defaultValue: "File Rewrite" })}</Label>
<Label>
{t("file-rewrite", {
defaultValue: "File Rewrite",
})}
</Label>
<Controller
control={control}
name="rewriteDescription"
@ -1197,37 +1267,43 @@ export default function FormVideo() {
</RadioGroup>
</>
)}
<Controller
control={control}
name="files"
render={({ field }) => (
<div className="py-3 space-y-2">
<Label>{t("select-file", { defaultValue: "Select File" })}</Label>
{/* <Input
id="fileInput"
type="file"
onChange={handleImageChange}
/> */}
<Fragment>
<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">
{/* Drop files here or click to upload. */}
{t("drag-file", { defaultValue: "Drag File" })}
</h4>
<div className="text-xs text-muted-foreground">
{t("upload-file-video-max", { defaultValue: "Upload File Video Max" })}
{t("upload-file-video-max", {
defaultValue: "Upload File Video Max",
})}
</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" />
{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> */}
))}
<div className="flex justify-between gap-2 mt-1">
<Button
color="destructive"
onClick={handleRemoveAllFiles}
@ -1235,17 +1311,23 @@ export default function FormVideo() {
{t("remove-all", { defaultValue: "Remove All" })}
</Button>
</div>
</Fragment>
) : null}
</Fragment>
</div>
</div>
)}
{/* Submit Button */}
{errors.files && (
<p className="text-red-500 text-sm">
{errors.files.message}
</p>
)}
</div>
)}
/>
</div>
</div>
</Card>
<div className="w-full lg:w-4/12">
<Card className=" h-[800px]">
<Card className="h-fit">
<div className="px-3 py-3">
<div className="space-y-2">
<Label>{t("creator", { defaultValue: "Creator" })}</Label>
@ -1283,17 +1365,33 @@ export default function FormVideo() {
</div>
)}
<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>
<Controller
control={control}
name="tags"
render={({ field }) => (
<>
<Input
type="text"
id="tags"
placeholder="Add a tag and press Enter"
onKeyDown={handleAddTag}
ref={inputRef}
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">
{tags.map((tag, index) => (
{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"
@ -1301,7 +1399,12 @@ export default function FormVideo() {
{tag}{" "}
<button
type="button"
onClick={() => handleRemoveTag(index)}
onClick={() => {
const updatedTags = field.value.filter(
(_, i) => i !== index
);
field.onChange(updatedTags);
}}
className="remove-tag-button"
>
×
@ -1309,10 +1412,20 @@ export default function FormVideo() {
</span>
))}
</div>
</>
)}
/>
{/* Tampilkan error */}
{errors.tags?.message && (
<p className="text-red-400 text-sm">{errors.tags.message}</p>
)}
</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>
<Label>
{t("publish-target", { defaultValue: "Publish Target" })}
</Label>
{options.map((option) => (
<div key={option.id} className="flex gap-2 items-center">
<Checkbox

View File

@ -93,7 +93,7 @@ export default function CreateSettingTracking() {
<Input
size={"md"}
type="number"
placeholder="Masukan Nama Iklan"
placeholder="Masukan Angka"
/>
</div>
</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"
},
"Form": {
"titleRequired": "Title required",
"creatorRequired": "Creator required",
"enterTitle": "Enter Title",
"no": "No",
"title": "Title",
"category-name": "Category Name",
@ -813,7 +816,7 @@
"view-file": "View File",
"update": "Update",
"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",
"file-rewrite": "File Rewrite",
"file-placement": "File Placement",
@ -849,6 +852,10 @@
"data-media": "please complete the data! ",
"title-media-online": "Online Media Name",
"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"
},
"Form": {
"titleRequired": "Judul diperlukan",
"creatorRequired": "Kreator diperlukan",
"enterTitle": "Masukkan Judul",
"no": "Nomor",
"title": "Judul",
"category-name": "Nama Kategori",
@ -814,7 +818,7 @@
"view-file": "Lihat file",
"update": "Edit",
"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",
"file-rewrite": "File hasil Rewrite",
"file-placement": "Penempatan file",
@ -849,6 +853,9 @@
"data-media": "Silahkan Lengkapi Data!",
"title-media-online": "Nama Media Online",
"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."
}
}