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()} />
<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({ 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. */}
<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-audio-max", { defaultValue: "Upload File Audio Max" })}
<div className="text-xs text-muted-foreground">
{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">
{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">
{/* 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",
})}
</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> */}
<div className="flex justify-between gap-2">
<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,50 +515,14 @@ 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 ">
<div className="mt-2">
<Swiper
onSwiper={setThumbsSwiper}
slidesPerView={8}
@ -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">
<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. */}
<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-text-max", { defaultValue: "Upload File Text Max" })}
<div className="text-xs text-muted-foreground">
{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> */}
<div className="flex justify-between gap-2">
<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,25 +1350,31 @@ 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) => (
<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"
className="px-1 py-1 rounded-lg bg-black text-white mr-2 text-sm font-sans"
>
{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">
<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. */}
<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-video-max", { defaultValue: "Upload File Video Max" })}
<div className="text-xs text-muted-foreground">
{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,25 +1365,46 @@ 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) => (
<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"
className="px-1 py-1 rounded-lg bg-black text-white mr-2 text-sm font-sans"
>
{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",
@ -789,7 +793,7 @@
"main-keyword": "Main Keyword",
"seo": "Seo",
"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",
"description": "Deskripsi",
"select-file": "Pilih File",
@ -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."
}
}