feat:fix SPIT, penugasan,agenda setting

This commit is contained in:
Anang Yusman 2025-01-21 18:30:21 +08:00
parent ade8b1b296
commit 079d238d1f
11 changed files with 743 additions and 166 deletions

View File

@ -21,7 +21,13 @@ import {
import { Calendar } from "@/components/ui/calendar"; import { Calendar } from "@/components/ui/calendar";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { Loader2, CalendarIcon, ChevronUp, ChevronDown } from "lucide-react"; import {
Loader2,
CalendarIcon,
ChevronUp,
ChevronDown,
Music,
} from "lucide-react";
import DeleteConfirmationDialog from "@/components/delete-confirmation-dialog"; import DeleteConfirmationDialog from "@/components/delete-confirmation-dialog";
import { CalendarCategory } from "./data"; import { CalendarCategory } from "./data";
import { import {
@ -338,11 +344,17 @@ const EventModal = ({
await uploadResumableFile(index, String(id), item, "3", "0"); await uploadResumableFile(index, String(id), item, "3", "0");
}); });
if (audioFiles?.length === 0) { if (audioFiles?.length == 0) {
setIsAudioUploadFinish(true); setIsAudioUploadFinish(true);
} }
audioFiles?.map(async (item: any, index: number) => { audioFiles.map(async (item: FileWithPreview, index: number) => {
await uploadResumableFile(index, String(id), item, "4", "0"); await uploadResumableFile(
index,
String(id),
item, // Use .file to access the actual File object
"4",
"0" // Optional: Replace with actual duration if available
);
}); });
}; };
@ -429,9 +441,15 @@ const EventModal = ({
audio.controls = true; audio.controls = true;
document.body.appendChild(audio); document.body.appendChild(audio);
// Convert Blob to File // Convert Blob to File and add preview
const file = new File([blob], "voiceNote.webm", { type: "audio/webm" }); const fileWithPreview: FileWithPreview = Object.assign(
setAudioFile(file); new File([blob], "voiceNote.webm", { type: "audio/webm" }),
{ preview: url }
);
// Add to state
setAudioFile(fileWithPreview);
setAudioFiles((prev) => [...prev, fileWithPreview]);
}; };
const handleDeleteAudio = () => { const handleDeleteAudio = () => {
@ -993,8 +1011,7 @@ const EventModal = ({
))} ))}
</div> </div>
<div> <div>
<Label>Voice Note</Label> <Label>Audio</Label>
<AudioRecorder <AudioRecorder
onRecordingComplete={addAudioElement} onRecordingComplete={addAudioElement}
audioTrackConstraints={{ audioTrackConstraints={{
@ -1011,7 +1028,9 @@ const EventModal = ({
}} }}
maxSize={100} maxSize={100}
label="Upload file dengan format .mp3 atau .wav." label="Upload file dengan format .mp3 atau .wav."
onDrop={(files) => setAudioFiles(files)} onDrop={(files) =>
setAudioFiles((prev) => [...prev, ...files])
}
className="mt-2" className="mt-2"
/> />
{audioUploadedFiles?.map((file: any, index: number) => ( {audioUploadedFiles?.map((file: any, index: number) => (
@ -1054,7 +1073,7 @@ const EventModal = ({
> >
<div className="flex gap-3 items-center"> <div className="flex gap-3 items-center">
<div className="file-preview"> <div className="file-preview">
{renderFilePreview(file.url)} <Music />
</div> </div>
<div> <div>
<div className=" text-sm text-card-foreground"> <div className=" text-sm text-card-foreground">
@ -1078,7 +1097,10 @@ const EventModal = ({
</div> </div>
{audioFile && ( {audioFile && (
<div className="flex flex-row justify-between items-center"> <div className="flex flex-row justify-between items-center">
<p>Voice note ready to submit: {audioFile.name}</p> <div className="flex items-center mr-1">
{" "}
<Music /> <p>Voice Note</p>
</div>
<Button <Button
type="button" type="button"
onClick={handleDeleteAudio} onClick={handleDeleteAudio}

View File

@ -0,0 +1,72 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import { Card, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Rows, Search, UploadIcon } from "lucide-react";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import { Link } from "@/components/navigation";
import { formatDateToIndonesian, generateLocalizedPath } from "@/utils/globals";
import { Icon } from "@iconify/react/dist/iconify.js";
import { locale } from "dayjs";
import { useEffect, useState } from "react";
import { getListContent } from "@/service/landing/landing";
import ContestTable from "../../../../contest/components/contest-table";
import AudioSliderPage from "../../audio/audio";
import TeksSliderPage from "../../document/teks";
import ImageSliderPage from "../../image/image";
import VideoSliderPage from "../../video/audio-visual";
const AudioAllPage = () => {
return (
<div>
<SiteBreadcrumb />
<div className="my-3">
<Tabs defaultValue="giat-routine" className="w-full">
<Card className="py-3 px-2 my-4 h-20 flex items-center">
<p className="text-lg font-semibold ml-2">Konten Audio</p>
</Card>
<TabsContent value="giat-routine">
<div className="grid grid-cols-12 gap-5">
<div className="lg:col-span-12 col-span-12">
<Card>
<div className="flex justify-between items-center py-4 px-5">
<div>
<InputGroup merged>
<InputGroupText className="bg-transparent dark:border-secondary dark:group-focus-within:border-secondary">
<Search className=" h-4 w-4 dark:text-white" />
</InputGroupText>
<Input
type="text"
placeholder="Search Judul..."
className="bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white"
/>
</InputGroup>
</div>
</div>
<div className="ml-5 pb-3">
<div className="flex justify-between items-center mx-3">
<Label className="text-base">Audio</Label>
</div>
<div className="px-5 my-5">
<AudioSliderPage />
</div>
</div>
</Card>
</div>
</div>
</TabsContent>
</Tabs>
</div>
</div>
);
};
export default AudioAllPage;

View File

@ -66,7 +66,7 @@ const AudioSliderPage = () => {
<Link <Link
href={`/shared/curated-content//giat-routine/audio/detail/${audio.id}`} href={`/shared/curated-content//giat-routine/audio/detail/${audio.id}`}
key={audio?.id} key={audio?.id}
className="flex flex-col sm:flex-row items-center hover:scale-110 transition-transform duration-300 bg-white dark:bg-gray-800 cursor-pointer shadow-md rounded-lg p-4 gap-4 w-full" className="flex flex-col sm:flex-row items-center hover:scale-100 transition-transform duration-300 bg-white dark:bg-gray-800 cursor-pointer shadow-md rounded-lg p-4 gap-4 w-full"
> >
<div className="flex items-center justify-center bg-red-500 text-white rounded-lg w-16 h-16"> <div className="flex items-center justify-center bg-red-500 text-white rounded-lg w-16 h-16">
<svg <svg

View File

@ -0,0 +1,119 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import { Card, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Rows, Search, UploadIcon } from "lucide-react";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import { Link } from "@/components/navigation";
import { formatDateToIndonesian, generateLocalizedPath } from "@/utils/globals";
import { Icon } from "@iconify/react/dist/iconify.js";
import { locale } from "dayjs";
import { useEffect, useState } from "react";
import { getListContent } from "@/service/landing/landing";
import ContestTable from "../../../../contest/components/contest-table";
import AudioSliderPage from "../../audio/audio";
import TeksSliderPage from "../../document/teks";
import ImageSliderPage from "../../image/image";
import VideoSliderPage from "../../video/audio-visual";
const DocumentAllPage = () => {
return (
<div>
<SiteBreadcrumb />
<div className="my-3">
<Tabs defaultValue="giat-routine" className="w-full">
<Card className="py-3 px-2 my-4 h-20 flex items-center">
<p className="text-lg font-semibold ml-2">Konten Teks</p>
</Card>
<TabsContent value="giat-routine">
<div className="grid grid-cols-12 gap-5">
<div className="lg:col-span-12 col-span-12">
<Card>
<div className="flex justify-between items-center py-4 px-5">
<div>
<InputGroup merged>
<InputGroupText className="bg-transparent dark:border-secondary dark:group-focus-within:border-secondary">
<Search className=" h-4 w-4 dark:text-white" />
</InputGroupText>
<Input
type="text"
placeholder="Search Judul..."
className="bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white"
/>
</InputGroup>
</div>
</div>
<div className="ml-5 pb-3">
<div className="flex justify-between items-center mx-3">
<Label className="text-base">Teks</Label>
</div>
<div className="px-5 my-5">
<TeksSliderPage />
</div>
</div>
</Card>
</div>
</div>
</TabsContent>
<TabsContent value="giat-penugasan">
<div className="grid grid-cols-12 gap-5">
<div className="lg:col-span-12 col-span-12">
<Card>
<div className="flex justify-between items-center py-4 px-5">
<div>
<InputGroup merged>
<InputGroupText className="bg-transparent dark:border-secondary dark:group-focus-within:border-secondary">
<Search className=" h-4 w-4 dark:text-white" />
</InputGroupText>
<Input
type="text"
placeholder="Search Judul..."
className="bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white"
/>
</InputGroup>
</div>
</div>
<div className="ml-5 pb-3">
<Label>Audio Visual</Label>
<div className="px-5 my-5">
<VideoSliderPage />
</div>
<Label>Audio</Label>
<div className="px-5 my-5">
<AudioSliderPage />
</div>
<Label>Foto</Label>
<div className="px-5 my-5">
<ImageSliderPage />
</div>
<Label>Teks</Label>
<div className="px-5 my-5">
<TeksSliderPage />
</div>
</div>
</Card>
</div>
</div>
</TabsContent>
<TabsContent value="contest">
<Card>
<div className="py-3">
<ContestTable />
</div>
</Card>
</TabsContent>
</Tabs>
</div>
</div>
);
};
export default DocumentAllPage;

View File

@ -0,0 +1,72 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import { Card, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Rows, Search, UploadIcon } from "lucide-react";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import { Link } from "@/components/navigation";
import { formatDateToIndonesian, generateLocalizedPath } from "@/utils/globals";
import { Icon } from "@iconify/react/dist/iconify.js";
import { locale } from "dayjs";
import { useEffect, useState } from "react";
import { getListContent } from "@/service/landing/landing";
import ContestTable from "../../../../contest/components/contest-table";
import AudioSliderPage from "../../audio/audio";
import TeksSliderPage from "../../document/teks";
import ImageSliderPage from "../../image/image";
import VideoSliderPage from "../../video/audio-visual";
const ImageAllPage = () => {
return (
<div>
<SiteBreadcrumb />
<div className="my-3">
<Tabs defaultValue="giat-routine" className="w-full">
<Card className="py-3 px-2 my-4 h-20 flex items-center">
<p className="text-lg font-semibold ml-2">Konten Image</p>
</Card>
<TabsContent value="giat-routine">
<div className="grid grid-cols-12 gap-5">
<div className="lg:col-span-12 col-span-12">
<Card>
<div className="flex justify-between items-center py-4 px-5">
<div>
<InputGroup merged>
<InputGroupText className="bg-transparent dark:border-secondary dark:group-focus-within:border-secondary">
<Search className=" h-4 w-4 dark:text-white" />
</InputGroupText>
<Input
type="text"
placeholder="Search Judul..."
className="bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white"
/>
</InputGroup>
</div>
</div>
<div className="ml-5 pb-3">
<div className="flex justify-between items-center mx-3">
<Label className="text-base">Image</Label>
</div>
<div className="px-5 my-5">
<ImageSliderPage />
</div>
</div>
</Card>
</div>
</div>
</TabsContent>
</Tabs>
</div>
</div>
);
};
export default ImageAllPage;

View File

@ -0,0 +1,119 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import { Card, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Rows, Search, UploadIcon } from "lucide-react";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import { Link } from "@/components/navigation";
import { formatDateToIndonesian, generateLocalizedPath } from "@/utils/globals";
import { Icon } from "@iconify/react/dist/iconify.js";
import { locale } from "dayjs";
import { useEffect, useState } from "react";
import { getListContent } from "@/service/landing/landing";
import ContestTable from "../../../../contest/components/contest-table";
import AudioSliderPage from "../../audio/audio";
import TeksSliderPage from "../../document/teks";
import ImageSliderPage from "../../image/image";
import VideoSliderPage from "../audio-visual";
const VideoAllPage = () => {
return (
<div>
<SiteBreadcrumb />
<div className="my-3">
<Tabs defaultValue="giat-routine" className="w-full">
<Card className="py-3 px-2 my-4 h-20 flex items-center">
<p className="text-lg font-semibold ml-2">Konten Video</p>
</Card>
<TabsContent value="giat-routine">
<div className="grid grid-cols-12 gap-5">
<div className="lg:col-span-12 col-span-12">
<Card>
<div className="flex justify-between items-center py-4 px-5">
<div>
<InputGroup merged>
<InputGroupText className="bg-transparent dark:border-secondary dark:group-focus-within:border-secondary">
<Search className=" h-4 w-4 dark:text-white" />
</InputGroupText>
<Input
type="text"
placeholder="Search Judul..."
className="bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white"
/>
</InputGroup>
</div>
</div>
<div className="ml-5 pb-3">
<div className="flex justify-between items-center mx-3">
<Label className="text-base">Audio Visual</Label>
</div>
<div className="px-5 my-5">
<VideoSliderPage />
</div>
</div>
</Card>
</div>
</div>
</TabsContent>
<TabsContent value="giat-penugasan">
<div className="grid grid-cols-12 gap-5">
<div className="lg:col-span-12 col-span-12">
<Card>
<div className="flex justify-between items-center py-4 px-5">
<div>
<InputGroup merged>
<InputGroupText className="bg-transparent dark:border-secondary dark:group-focus-within:border-secondary">
<Search className=" h-4 w-4 dark:text-white" />
</InputGroupText>
<Input
type="text"
placeholder="Search Judul..."
className="bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white"
/>
</InputGroup>
</div>
</div>
<div className="ml-5 pb-3">
<Label>Audio Visual</Label>
<div className="px-5 my-5">
<VideoSliderPage />
</div>
<Label>Audio</Label>
<div className="px-5 my-5">
<AudioSliderPage />
</div>
<Label>Foto</Label>
<div className="px-5 my-5">
<ImageSliderPage />
</div>
<Label>Teks</Label>
<div className="px-5 my-5">
<TeksSliderPage />
</div>
</div>
</Card>
</div>
</div>
</TabsContent>
<TabsContent value="contest">
<Card>
<div className="py-3">
<ContestTable />
</div>
</Card>
</TabsContent>
</Tabs>
</div>
</div>
);
};
export default VideoAllPage;

View File

@ -1,7 +1,7 @@
import SiteBreadcrumb from "@/components/site-breadcrumb"; import SiteBreadcrumb from "@/components/site-breadcrumb";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Search, UploadIcon } from "lucide-react"; import { Rows, Search, UploadIcon } from "lucide-react";
import { InputGroup, InputGroupText } from "@/components/ui/input-group"; import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
@ -73,19 +73,69 @@ const CuratedContentPage = () => {
</div> </div>
</div> </div>
<div className="ml-5 pb-3"> <div className="ml-5 pb-3">
<Label>Audio Visual</Label> <div className="flex justify-between items-center mx-3">
<Label className="text-base">Audio Visual</Label>
<Label>
{" "}
<Link
href={
"/shared/curated-content/giat-routine/video/all"
}
>
Lihat Semua
</Link>
</Label>
</div>
<div className="px-5 my-5"> <div className="px-5 my-5">
<VideoSliderPage /> <VideoSliderPage />
</div> </div>
<Label>Audio</Label> <div className="flex justify-between items-center mx-3">
<Label className="text-base">Audio</Label>
<Label>
{" "}
<Link
href={
"/shared/curated-content/giat-routine/audio/all"
}
>
Lihat Semua
</Link>
</Label>
</div>
<div className="px-5 my-5"> <div className="px-5 my-5">
<AudioSliderPage /> <AudioSliderPage />
</div> </div>
<Label>Foto</Label> <div className="flex justify-between items-center mx-3">
<Label className="text-base">Foto</Label>
<Label>
{" "}
<Link
href={
"/shared/curated-content/giat-routine/image/all"
}
>
Lihat Semua
</Link>
</Label>
</div>
<div className="px-5 my-5"> <div className="px-5 my-5">
<ImageSliderPage /> <ImageSliderPage />
</div> </div>
<Label>Teks</Label> <div className="flex justify-between items-center mx-3">
<Label className="text-base">Teks</Label>
<Label>
<Link
href={
"/shared/curated-content/giat-routine/document/all"
}
>
Lihat Semua
</Link>
</Label>
</div>
<div className="px-5 my-5"> <div className="px-5 my-5">
<TeksSliderPage /> <TeksSliderPage />
</div> </div>

View File

@ -96,6 +96,13 @@ interface PlacementData {
placements: string; placements: string;
} }
type FileType = {
contentId: number;
contentFile: string;
thumbnailFileUrl: string;
fileName: string;
};
const CustomEditor = dynamic( const CustomEditor = dynamic(
() => { () => {
return import("@/components/editor/custom-editor"); return import("@/components/editor/custom-editor");
@ -122,7 +129,7 @@ export default function FormConvertSPIT() {
null null
); );
const [tags, setTags] = useState<any[]>([]); const [tags, setTags] = useState<any[]>([]);
const [detail, setDetail] = useState<Detail>(); const [detail, setDetail] = useState<any>();
const [refresh, setRefresh] = useState(false); const [refresh, setRefresh] = useState(false);
const [selectedPublishers, setSelectedPublishers] = useState<number[]>([]); const [selectedPublishers, setSelectedPublishers] = useState<number[]>([]);
const [detailThumb, setDetailThumb] = useState<any>([]); const [detailThumb, setDetailThumb] = useState<any>([]);
@ -150,7 +157,9 @@ export default function FormConvertSPIT() {
polres: false, polres: false,
}); });
const [publishedFor, setPublishedFor] = useState<string[]>([]); const [publishedFor, setPublishedFor] = useState<string[]>([]);
const [placementLength, setPlacementLength] = useState([]); const [filePlacements, setFilePlacements] = useState<string[][]>([]);
const [isUserMabesApprover, setIsUserMabesApprover] = useState(false);
const [files, setFiles] = useState<FileType[]>([]);
const options: Option[] = [ const options: Option[] = [
{ id: "all", label: "SEMUA" }, { id: "all", label: "SEMUA" },
@ -201,6 +210,17 @@ export default function FormConvertSPIT() {
initState(); initState();
}, []); }, []);
useEffect(() => {
if (
userLevelId != undefined &&
roleId != undefined &&
userLevelId == "216" &&
roleId == "3"
) {
setIsUserMabesApprover(true);
}
}, [userLevelId, roleId]);
useEffect(() => { useEffect(() => {
if ( if (
userLevelId != undefined && userLevelId != undefined &&
@ -237,6 +257,47 @@ export default function FormConvertSPIT() {
} }
}; };
const setupPlacement = (
index: number,
placement: string,
checked: boolean
) => {
let temp = [...filePlacements];
if (checked) {
if (placement === "all") {
temp[index] = ["all", "mabes", "polda", "international"];
} else {
const now = temp[index];
now.push(placement);
if (now.length === 3 && !now.includes("all")) {
now.push("all");
}
temp[index] = now;
}
} else {
if (placement === "all") {
temp[index] = [];
} else {
const now = temp[index].filter((a) => a !== placement);
console.log("now", now);
temp[index] = now;
if (now.length === 3 && now.includes("all")) {
const newData = now.filter((b) => b !== "all");
temp[index] = newData;
}
}
}
setFilePlacements(temp);
};
const setupPlacementCheck = (length: number) => {
const temp = [];
for (let i = 0; i < length; i++) {
temp.push([]);
}
setFilePlacements(temp);
};
useEffect(() => { useEffect(() => {
async function initState() { async function initState() {
if (id) { if (id) {
@ -244,6 +305,8 @@ export default function FormConvertSPIT() {
const details = response?.data?.data; const details = response?.data?.data;
setDetail(details); setDetail(details);
setFiles(details?.contentList);
setupPlacementCheck(details?.contentList?.length);
const filesData = details.contentList || []; const filesData = details.contentList || [];
const fileUrls = filesData.map((file: { contentFile: string }) => const fileUrls = filesData.map((file: { contentFile: string }) =>
@ -272,49 +335,6 @@ export default function FormConvertSPIT() {
})) }))
); );
const getPlacement = (): PlacementData[] => {
return tempFile
.filter((file: FileData) => (file.placement || []).length > 0) // Gunakan default array
.map((file: FileData) => ({
mediaFileId: Number(file.contentId),
placements: (file.placement || []).join(","), // Gunakan default array
}));
};
const setupPlacement = (value: string, id: number): void => {
const updatedFiles = tempFile.map((file: FileData) => {
if (file.contentId === id) {
const currentPlacement = file.placement || [];
if (currentPlacement.includes(value)) {
// Remove the placement value
file.placement = currentPlacement.filter((val) => val !== value);
} else {
// Add the placement value
file.placement =
value === "all"
? ["all", "mabes", "polda", "international"]
: [...currentPlacement, value];
if (file.placement.includes("all") && value !== "all") {
file.placement = file.placement.filter((val) => val !== "all");
}
}
}
return file;
});
const placementLength = updatedFiles.reduce(
(acc: any, file: any) => acc + (file.placement?.length || 0),
0
);
setTempFile(
updatedFiles.sort((a: any, b: any) => a.contentId - b.contentId)
);
setPlacementLength(placementLength);
console.log("Updated Files:", updatedFiles);
};
const handleCheckboxChangeFile = (contentId: number, value: string) => { const handleCheckboxChangeFile = (contentId: number, value: string) => {
setTempFile((prevTempFile: any) => { setTempFile((prevTempFile: any) => {
return prevTempFile.map((file: any) => { return prevTempFile.map((file: any) => {
@ -359,12 +379,32 @@ export default function FormConvertSPIT() {
} }
}; };
const getPlacement = () => {
console.log("getPlaa", filePlacements);
const temp = [];
for (let i = 0; i < filePlacements?.length; i++) {
if (filePlacements[i].length !== 0) {
const now = filePlacements[i].filter((a) => a !== "all");
const data = {
mediaFileId: files[i].contentId,
placement: now.join(","),
};
temp.push(data);
}
}
return temp;
};
const save = async (data: { const save = async (data: {
contentTitle: string; contentTitle: string;
contentDescription: string; contentDescription: string;
contentRewriteDescription: string; contentRewriteDescription: string;
contentCreator: string; contentCreator: string;
}): Promise<void> => { }): Promise<void> => {
const temp = [];
for (const element of detail.contentList) {
temp.push([]);
}
const description = const description =
selectedFileType === "original" selectedFileType === "original"
? data.contentDescription ? data.contentDescription
@ -376,15 +416,16 @@ export default function FormConvertSPIT() {
description, description,
htmlDescription: description, htmlDescription: description,
tags: "siap", tags: "siap",
categoryId: selectedCategoryId, categoryId: 1,
publishedFor: publishedFor.join(","), publishedFor: publishedFor.join(","),
creator: data.contentCreator, creator: data.contentCreator,
files: getPlacement(), // Include placement data files: isUserMabesApprover ? getPlacement() : [], // Include placement data
}; };
const response = await convertSPIT(requestData); const response = await convertSPIT(requestData);
console.log("Form Data Submitted:", response); console.log("Form Data Submitted:", response);
setFilePlacements(temp);
setFiles(detail.files);
MySwal.fire({ MySwal.fire({
title: "Sukses", title: "Sukses",
text: "Data berhasil disimpan.", text: "Data berhasil disimpan.",
@ -699,62 +740,96 @@ export default function FormConvertSPIT() {
</div> </div>
</div> </div>
</div> </div>
{isMabesApprover ? ( {files?.map((file, index) => (
<div className="mt-5"> <div
<Label className="text-xl text-black"> key={file.contentId}
Penempatan File className="flex flex-row gap-2 items-center my-3"
</Label> >
{detailThumb.map((data: any) => ( <img
<div src={file.contentFile}
key={data.contentId} className="w-[150px] rounded-md"
className="flex items-center gap-3 mt-2" />
> <div className="flex flex-col gap-2 w-full">
<img <div className="flex justify-between text-sm">
className="object-cover w-36 h-32" {file.fileName}
src={data} {/* <a
alt={`Thumbnail ${data.contentId}`} onClick={() =>
/> handleDeleteFileApproval(file.id)
<div className="flex flex-row gap-3 items-center"> }
{[ >
"all", <Icon icon="humbleicons:times" color="red" />
"mabes", </a> */}
"polda", </div>
"satker",
"internasional", <div className="flex flex-row gap-2">
].map((value) => ( <div className="flex items-center space-x-2">
<label <Checkbox
key={value} id="terms"
className="text-blue-500 cursor-pointer flex items-center gap-1" value="all"
> checked={filePlacements[index]?.includes("all")}
<input onCheckedChange={(e) =>
type="checkbox" setupPlacement(index, "all", Boolean(e))
name="placement" }
value={value} />
onChange={() => <label
handleCheckboxChangeFile( htmlFor="terms"
data.contentId, className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
value >
) Semua
} </label>
checked={ </div>
tempFile <div className="flex items-center space-x-2">
.find( <Checkbox
(file: FileData) => id="terms"
file.contentId === data.contentId checked={filePlacements[index]?.includes("mabes")}
) onCheckedChange={(e) =>
?.placement?.includes(value) || false setupPlacement(index, "mabes", Boolean(e))
} }
/> />
{value.charAt(0).toUpperCase() + value.slice(1)} <label
</label> htmlFor="terms"
))} className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Nasional
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="terms"
checked={filePlacements[index]?.includes("polda")}
onCheckedChange={(e) =>
setupPlacement(index, "polda", Boolean(e))
}
/>
<label
htmlFor="terms"
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Wilayah
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="terms"
checked={filePlacements[index]?.includes(
"international"
)}
onCheckedChange={(e) =>
setupPlacement(index, "international", Boolean(e))
}
/>
<label
htmlFor="terms"
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Internasional
</label>
</div> </div>
</div> </div>
))} </div>
</div> </div>
) : ( ))}
""
)}
</div> </div>
</div> </div>
</Card> </Card>
@ -797,14 +872,16 @@ export default function FormConvertSPIT() {
<div className="space-y-2"> <div className="space-y-2">
<Label>Tag</Label> <Label>Tag</Label>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{detail?.contentTag?.split(",").map((tag, index) => ( {detail?.contentTag
<Badge ?.split(",")
key={index} .map((tag: any, index: any) => (
className="border rounded-md px-2 py-2" <Badge
> key={index}
{tag.trim()} className="border rounded-md px-2 py-2"
</Badge> >
))} {tag.trim()}
</Badge>
))}
</div> </div>
</div> </div>
</div> </div>

View File

@ -40,7 +40,16 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { ChevronDown, ChevronUp, DotSquare, TrashIcon } from "lucide-react"; import {
ChevronDown,
ChevronUp,
Dock,
DotSquare,
ImageIcon,
Music,
TrashIcon,
VideoIcon,
} from "lucide-react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { Link } from "@/components/navigation"; import { Link } from "@/components/navigation";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
@ -1064,7 +1073,7 @@ export default function FormTaskDetail() {
onClick={() => setSelectedVideo(file.url)} onClick={() => setSelectedVideo(file.url)}
> >
<div className="file-preview"> <div className="file-preview">
{renderFilePreview(file.url)} <VideoIcon />
</div> </div>
<div> <div>
<div className="text-sm text-card-foreground"> <div className="text-sm text-card-foreground">
@ -1109,7 +1118,7 @@ export default function FormTaskDetail() {
onClick={() => setSelectedImage(file.url)} onClick={() => setSelectedImage(file.url)}
> >
<div className="file-preview"> <div className="file-preview">
{renderFilePreview(file.url)} <ImageIcon />
</div> </div>
<div> <div>
<div className="text-sm text-card-foreground"> <div className="text-sm text-card-foreground">
@ -1157,7 +1166,7 @@ export default function FormTaskDetail() {
onClick={() => setSelectedText(file.url)} onClick={() => setSelectedText(file.url)}
> >
<div className="file-preview"> <div className="file-preview">
{renderFilePreview(file.url)} <Dock />
</div> </div>
<div> <div>
<div className="text-sm text-card-foreground"> <div className="text-sm text-card-foreground">
@ -1228,7 +1237,7 @@ export default function FormTaskDetail() {
onClick={() => setSelectedAudio(file.url)} onClick={() => setSelectedAudio(file.url)}
> >
<div className="file-preview"> <div className="file-preview">
{renderFilePreview(file.url)} <Music />
</div> </div>
<div> <div>
<div className="text-sm text-card-foreground"> <div className="text-sm text-card-foreground">

View File

@ -32,7 +32,14 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { ChevronDown, ChevronUp } from "lucide-react"; import {
ChevronDown,
ChevronUp,
Dock,
ImageIcon,
Music,
VideoIcon,
} from "lucide-react";
import FileUploader from "../shared/file-uploader"; import FileUploader from "../shared/file-uploader";
import { AudioRecorder } from "react-audio-voice-recorder"; import { AudioRecorder } from "react-audio-voice-recorder";
import Image from "next/image"; import Image from "next/image";
@ -376,8 +383,14 @@ export default function FormTaskEdit() {
if (audioFiles?.length == 0) { if (audioFiles?.length == 0) {
setIsAudioUploadFinish(true); setIsAudioUploadFinish(true);
} }
audioFiles?.map(async (item: any, index: number) => { audioFiles.map(async (item: FileWithPreview, index: number) => {
await uploadResumableFile(index, String(id), item, "4", "0"); await uploadResumableFile(
index,
String(id),
item, // Use .file to access the actual File object
"4",
"0" // Optional: Replace with actual duration if available
);
}); });
// MySwal.fire({ // MySwal.fire({
@ -446,10 +459,17 @@ export default function FormTaskEdit() {
audio.controls = true; audio.controls = true;
document.body.appendChild(audio); document.body.appendChild(audio);
// Convert Blob to File // Convert Blob to File and add preview
const file = new File([blob], "voiceNote.webm", { type: "audio/webm" }); const fileWithPreview: FileWithPreview = Object.assign(
setAudioFile(file); new File([blob], "voiceNote.webm", { type: "audio/webm" }),
{ preview: url }
);
// Add to state
setAudioFile(fileWithPreview);
setAudioFiles((prev) => [...prev, fileWithPreview]);
}; };
const handleDeleteAudio = () => { const handleDeleteAudio = () => {
// Remove the audio file by setting state to null // Remove the audio file by setting state to null
setAudioFile(null); setAudioFile(null);
@ -835,7 +855,7 @@ export default function FormTaskEdit() {
<Label htmlFor="attachments">Lampiran</Label> <Label htmlFor="attachments">Lampiran</Label>
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
{videoUploadedFiles?.length > 0 && <Label>Video</Label>} <Label>Video</Label>
<FileUploader <FileUploader
accept={{ accept={{
"mp4/*": [], "mp4/*": [],
@ -843,7 +863,7 @@ export default function FormTaskEdit() {
}} }}
maxSize={100} maxSize={100}
label="Upload file dengan format .mp4 atau .mov." label="Upload file dengan format .mp4 atau .mov."
onDrop={(files) => setImageFiles(files)} onDrop={(files) => setVideoFiles(files)}
/> />
{videoUploadedFiles?.map((file: any, index: number) => ( {videoUploadedFiles?.map((file: any, index: number) => (
<div> <div>
@ -853,7 +873,7 @@ export default function FormTaskEdit() {
> >
<div className="flex gap-3 items-center"> <div className="flex gap-3 items-center">
<div className="file-preview"> <div className="file-preview">
{renderFilePreview(file.url)} <VideoIcon />
</div> </div>
<div> <div>
<div className=" text-sm text-card-foreground"> <div className=" text-sm text-card-foreground">
@ -876,7 +896,7 @@ export default function FormTaskEdit() {
))} ))}
</div> </div>
<div> <div>
{imageUploadedFiles?.length > 0 && <Label>Foto</Label>} <Label>Foto</Label>
<FileUploader <FileUploader
accept={{ accept={{
"image/*": [], "image/*": [],
@ -893,7 +913,7 @@ export default function FormTaskEdit() {
> >
<div className="flex gap-3 items-center"> <div className="flex gap-3 items-center">
<div className="file-preview"> <div className="file-preview">
{renderFilePreview(file.url)} <ImageIcon />
</div> </div>
<div> <div>
<div className=" text-sm text-card-foreground"> <div className=" text-sm text-card-foreground">
@ -916,7 +936,7 @@ export default function FormTaskEdit() {
))} ))}
</div> </div>
<div> <div>
{textUploadedFiles?.length > 0 && <Label>Teks</Label>} <Label>Teks</Label>
<FileUploader <FileUploader
accept={{ accept={{
"pdf/*": [], "pdf/*": [],
@ -933,7 +953,7 @@ export default function FormTaskEdit() {
> >
<div className="flex gap-3 items-center"> <div className="flex gap-3 items-center">
<div className="file-preview"> <div className="file-preview">
{renderFilePreview(file.url)} <Dock />
</div> </div>
<div> <div>
<div className=" text-sm text-card-foreground"> <div className=" text-sm text-card-foreground">
@ -956,7 +976,7 @@ export default function FormTaskEdit() {
))} ))}
</div> </div>
<div> <div>
{audioUploadedFiles?.length > 0 && <Label>Audio</Label>} <Label>Audio</Label>
<AudioRecorder <AudioRecorder
onRecordingComplete={addAudioElement} onRecordingComplete={addAudioElement}
audioTrackConstraints={{ audioTrackConstraints={{
@ -973,7 +993,9 @@ export default function FormTaskEdit() {
}} }}
maxSize={100} maxSize={100}
label="Upload file dengan format .mp3 atau .wav." label="Upload file dengan format .mp3 atau .wav."
onDrop={(files) => setAudioFiles(files)} onDrop={(files) =>
setAudioFiles((prev) => [...prev, ...files])
}
className="mt-2" className="mt-2"
/> />
{audioUploadedFiles?.map((file: any, index: number) => ( {audioUploadedFiles?.map((file: any, index: number) => (
@ -984,7 +1006,7 @@ export default function FormTaskEdit() {
> >
<div className="flex gap-3 items-center"> <div className="flex gap-3 items-center">
<div className="file-preview"> <div className="file-preview">
{renderFilePreview(file.url)} <Music />
</div> </div>
<div> <div>
<div className=" text-sm text-card-foreground"> <div className=" text-sm text-card-foreground">
@ -1008,7 +1030,7 @@ export default function FormTaskEdit() {
</div> </div>
{audioFile && ( {audioFile && (
<div className="flex flex-row justify-between items-center"> <div className="flex flex-row justify-between items-center">
<p>Voice note ready to submit: {audioFile.name}</p> <p>Voice Note</p>
<Button <Button
type="button" type="button"
onClick={handleDeleteAudio} onClick={handleDeleteAudio}

View File

@ -275,8 +275,14 @@ export default function FormTask() {
if (audioFiles?.length == 0) { if (audioFiles?.length == 0) {
setIsAudioUploadFinish(true); setIsAudioUploadFinish(true);
} }
audioFiles?.map(async (item: any, index: number) => { audioFiles.map(async (item: FileWithPreview, index: number) => {
await uploadResumableFile(index, String(id), item, "4", "0"); await uploadResumableFile(
index,
String(id),
item, // Use .file to access the actual File object
"4",
"0" // Optional: Replace with actual duration if available
);
}); });
}; };
@ -335,15 +341,19 @@ export default function FormTask() {
audio.controls = true; audio.controls = true;
document.body.appendChild(audio); document.body.appendChild(audio);
// Convert Blob to File // Convert Blob to File and add preview
const file = new File([blob], "voiceNote.webm", { type: "audio/webm" }); const fileWithPreview: FileWithPreview = Object.assign(
setAudioFile(file); new File([blob], "voiceNote.webm", { type: "audio/webm" }),
{ preview: url }
);
// Add to state
setAudioFile(fileWithPreview);
setAudioFiles((prev) => [...prev, fileWithPreview]);
}; };
const handleDeleteAudio = () => {
// Remove the audio file by setting state to null const handleDeleteAudio = (index: number) => {
setAudioFile(null); setAudioFiles((prev) => prev.filter((_, idx) => idx !== index));
const audioElements = document.querySelectorAll("audio");
audioElements.forEach((audio) => audio.remove());
}; };
async function uploadResumableFile( async function uploadResumableFile(
@ -752,7 +762,7 @@ export default function FormTask() {
/> />
</div> </div>
<div> <div>
<Label>Voice Note</Label> <Label>Audio</Label>
<AudioRecorder <AudioRecorder
onRecordingComplete={addAudioElement} onRecordingComplete={addAudioElement}
audioTrackConstraints={{ audioTrackConstraints={{
@ -769,23 +779,28 @@ export default function FormTask() {
}} }}
maxSize={100} maxSize={100}
label="Upload file dengan format .mp3 atau .wav." label="Upload file dengan format .mp3 atau .wav."
onDrop={(files) => setAudioFiles(files)} onDrop={(files) =>
setAudioFiles((prev) => [...prev, ...files])
}
className="mt-2" className="mt-2"
/> />
</div> </div>
{audioFile && ( {audioFiles?.map((audio: any, idx: any) => (
<div className="flex flex-row justify-between items-center"> <div
<p>Voice note ready to submit: {audioFile.name}</p> key={idx}
className="flex flex-row justify-between items-center"
>
<p>Voice Note</p>
<Button <Button
type="button" type="button"
onClick={handleDeleteAudio} onClick={() => handleDeleteAudio(idx)}
size="sm" size="sm"
color="destructive" color="destructive"
> >
X X
</Button> </Button>
</div> </div>
)} ))}
{isRecording && <p>Recording... {timer} seconds remaining</p>}{" "} {isRecording && <p>Recording... {timer} seconds remaining</p>}{" "}
{/* Display remaining time */} {/* Display remaining time */}
<div className="mt-4"> <div className="mt-4">