Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign
This commit is contained in:
commit
e280a68635
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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")}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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")}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ export default function CreateSettingTracking() {
|
|||
<Input
|
||||
size={"md"}
|
||||
type="number"
|
||||
placeholder="Masukan Nama Iklan"
|
||||
placeholder="Masukan Angka"
|
||||
/>
|
||||
</div>
|
||||
</FormItem>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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."
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue