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 { zodResolver } from "@hookform/resolvers/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 { CalendarCategory } from "./data";
import {
@ -338,11 +344,17 @@ const EventModal = ({
await uploadResumableFile(index, String(id), item, "3", "0");
});
if (audioFiles?.length === 0) {
if (audioFiles?.length == 0) {
setIsAudioUploadFinish(true);
}
audioFiles?.map(async (item: any, index: number) => {
await uploadResumableFile(index, String(id), item, "4", "0");
audioFiles.map(async (item: FileWithPreview, index: number) => {
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;
document.body.appendChild(audio);
// Convert Blob to File
const file = new File([blob], "voiceNote.webm", { type: "audio/webm" });
setAudioFile(file);
// Convert Blob to File and add preview
const fileWithPreview: FileWithPreview = Object.assign(
new File([blob], "voiceNote.webm", { type: "audio/webm" }),
{ preview: url }
);
// Add to state
setAudioFile(fileWithPreview);
setAudioFiles((prev) => [...prev, fileWithPreview]);
};
const handleDeleteAudio = () => {
@ -993,8 +1011,7 @@ const EventModal = ({
))}
</div>
<div>
<Label>Voice Note</Label>
<Label>Audio</Label>
<AudioRecorder
onRecordingComplete={addAudioElement}
audioTrackConstraints={{
@ -1011,7 +1028,9 @@ const EventModal = ({
}}
maxSize={100}
label="Upload file dengan format .mp3 atau .wav."
onDrop={(files) => setAudioFiles(files)}
onDrop={(files) =>
setAudioFiles((prev) => [...prev, ...files])
}
className="mt-2"
/>
{audioUploadedFiles?.map((file: any, index: number) => (
@ -1054,7 +1073,7 @@ const EventModal = ({
>
<div className="flex gap-3 items-center">
<div className="file-preview">
{renderFilePreview(file.url)}
<Music />
</div>
<div>
<div className=" text-sm text-card-foreground">
@ -1078,7 +1097,10 @@ const EventModal = ({
</div>
{audioFile && (
<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
type="button"
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
href={`/shared/curated-content//giat-routine/audio/detail/${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">
<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 { Card, CardContent } from "@/components/ui/card";
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 { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@ -73,19 +73,69 @@ const CuratedContentPage = () => {
</div>
</div>
<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">
<VideoSliderPage />
</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">
<AudioSliderPage />
</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">
<ImageSliderPage />
</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">
<TeksSliderPage />
</div>

View File

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

View File

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

View File

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

View File

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