This commit is contained in:
Sabda Yagra 2025-07-14 23:34:52 +07:00
parent cd80bd07cb
commit aea083c0ea
24 changed files with 1677 additions and 757 deletions

View File

@ -119,8 +119,6 @@ const CalendarView = ({ categories }: CalendarViewProps) => {
const [calendarEvents, setCalendarEvents] = useState<CalendarEvent[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const t = useTranslations("CalendarApp");
// Modal states
const [sheetOpen, setSheetOpen] = useState<boolean>(false);
const [date, setDate] = useState<Date>(new Date());
const [activeView, setActiveView] = useState("listYear");
@ -132,7 +130,6 @@ const CalendarView = ({ categories }: CalendarViewProps) => {
dayjs(new Date(Number(INITIAL_YEAR), Number(INITIAL_MONTH) - 1, 1))
);
// Load events when view or month changes
useEffect(() => {
if (activeView === "listYear") {
getYearlyEvents();
@ -141,7 +138,6 @@ const CalendarView = ({ categories }: CalendarViewProps) => {
}
}, [activeView, selectedMonth]);
// Initialize selected categories
useEffect(() => {
if (categories?.length > 0) {
setSelectedCategory(categories.map((c) => c.value));
@ -162,11 +158,9 @@ const CalendarView = ({ categories }: CalendarViewProps) => {
return;
}
// For monthly view, we need to extract events from the specific month
const monthData = res?.data?.data;
if (monthData) {
const allEvents: CalendarEvent[] = [];
// Map API data to the calendarEvents structure
const events = monthData?.map((event: any) => ({
id: event.id.toString(),
title: event.title,
@ -208,7 +202,6 @@ const CalendarView = ({ categories }: CalendarViewProps) => {
return;
}
// The API already returns data organized by months
const yearlyData = res?.data?.data;
if (yearlyData) {
setYearlyData(yearlyData);
@ -221,7 +214,6 @@ const CalendarView = ({ categories }: CalendarViewProps) => {
}
};
// Filter events based on selected categories
const filteredEvents = calendarEvents.filter((event) => {
if (!selectedCategory.length) return false;
console.log("Event category : ", selectedCategory);
@ -231,7 +223,6 @@ const CalendarView = ({ categories }: CalendarViewProps) => {
.map((val: string) => val.trim());
const allCategoryId = ["1", "2", "3", "4", "5"];
// Cek apakah SEMUA validTypeIds ada di typeIdsInData
const hasAllCategories = allCategoryId.every((categoryId) =>
selectedCategory.includes(categoryId)
);
@ -242,7 +233,6 @@ const CalendarView = ({ categories }: CalendarViewProps) => {
);
});
// Event handlers
const handleEventClick = (arg: any) => {
setSelectedEventDate(null);
setSelectedEventData(arg);
@ -307,7 +297,6 @@ const CalendarView = ({ categories }: CalendarViewProps) => {
wait().then(() => (document.body.style.pointerEvents = "auto"));
};
// Utility functions
const getEventColor = (type: EventType): string => {
const typeSplit = type.split(",");
const firstType = typeSplit[0] as EventType;

View File

@ -234,7 +234,6 @@ const EventModal = ({
setWilayahPublish(wilayahState);
// Atur unit berdasarkan agendaType
if (rawAgendaTypes.includes("2")) {
setSelectedPolda(assignedToLevel);
}
@ -272,8 +271,6 @@ const EventModal = ({
const toggleWilayah = (key: string) => {
setWilayahPublish((prev: any) => {
let newState = { ...prev };
// Jika key === semua dan sebelumnya belum aktif, aktifkan semua
if (key === "semua") {
const newChecked = !prev.semua;
newState = {
@ -294,11 +291,9 @@ const EventModal = ({
return newState;
}
// Jika key bukan "semua"
newState[key] = !prev[key];
newState.semua = false; // Uncheck "semua" jika yang dipilih adalah individu
newState.semua = false;
// Hitung ulang agendaType berdasarkan pilihan
const selectedKeys = Object.entries(newState)
.filter(([k, v]) => v && k !== "semua")
.map(([k]) => wilayahValueMap[k]);
@ -348,8 +343,8 @@ const EventModal = ({
id: detailData?.id,
title: data.title,
description: data.description,
agendaType: agendaTypeList.join(","), // <-- ubah array jadi string
assignedToLevel: assignedToLevelList.join(","), // <-- ubah array jadi string
agendaType: agendaTypeList.join(","),
assignedToLevel: assignedToLevelList.join(","),
startDate: format(startDate, "yyyy-MM-dd"),
endDate: format(endDate, "yyyy-MM-dd"),
};
@ -393,9 +388,9 @@ const EventModal = ({
await uploadResumableFile(
index,
String(id),
item, // Use .file to access the actual File object
item,
"4",
"0" // Optional: Replace with actual duration if available
"0"
);
});
if (publish) {
@ -466,28 +461,26 @@ const EventModal = ({
const onRecordingStart = () => {
setIsRecording(true);
// Start a timer that stops the recording after 2 minutes (120 seconds)
const countdown = setInterval(() => {
setTimer((prevTimer) => {
if (prevTimer <= 1) {
clearInterval(countdown); // Stop the timer when it reaches 0
clearInterval(countdown);
return 0;
}
return prevTimer - 1;
});
}, 1000); // Update every second
}, 1000);
// Automatically stop recording after 2 minutes
setTimeout(() => {
if (isRecording) {
handleStopRecording();
}
}, 120000); // Stop after 2 minutes (120,000 ms)
}, 120000);
};
const handleStopRecording = () => {
setIsRecording(false);
setTimer(120); // Reset the timer to 2 minutes for the next recording
setTimer(120);
};
const addAudioElement = (blob: Blob) => {
@ -497,19 +490,16 @@ const EventModal = ({
audio.controls = true;
document.body.appendChild(audio);
// 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());
@ -842,7 +832,7 @@ const EventModal = ({
{wilayahPublish.polda && (
<UnitMapping
unit="Polda"
isDetail={isDetailMode} // jika Anda punya kondisi detail
isDetail={isDetailMode}
initData={selectedPolda}
sendDataToParent={(data: any) =>
setSelectedPolda(data)

View File

@ -123,7 +123,6 @@ export function UnitMapping(props: {
? isAllSatkerChecked
: isAllPolresChecked
}
disabled={isDetail}
onCheckedChange={(checked) => {
if (checked) {
form.setValue(
@ -139,6 +138,7 @@ export function UnitMapping(props: {
}
}}
/>
<label htmlFor="all" className="text-sm text-black uppercase">
SEMUA {unit}
</label>
@ -170,7 +170,6 @@ export function UnitMapping(props: {
>
<FormControl>
<Checkbox
disabled={isDetail}
checked={field.value?.includes(String(item.id))}
onCheckedChange={(checked) => {
return checked

View File

@ -62,11 +62,12 @@ const BlogTable = () => {
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [showData, setShowData] = React.useState("50");
const [showData, setShowData] = React.useState("10");
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: Number(showData),
pageSize: 10,
});
const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1);
const [limit, setLimit] = React.useState(10);
@ -143,14 +144,12 @@ const BlogTable = () => {
}
const handleCheckboxChange = (categoryId: number) => {
setSelectedCategories(
(prev: any) =>
setSelectedCategories((prev: any) =>
prev.includes(categoryId)
? prev.filter((id: any) => id !== categoryId) // Hapus jika sudah dipilih
: [...prev, categoryId] // Tambahkan jika belum dipilih
? prev.filter((id: any) => id !== categoryId)
: [...prev, categoryId]
);
// Perbarui filter kategori
setCategoryFilter((prev) => {
const updatedCategories = prev.split(",").filter(Boolean).map(Number);
@ -171,8 +170,8 @@ const BlogTable = () => {
}
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value); // Perbarui state search
table.getColumn("judul")?.setFilterValue(e.target.value); // Set filter tabel
setSearch(e.target.value);
table.getColumn("judul")?.setFilterValue(e.target.value);
};
return (
@ -181,7 +180,8 @@ const BlogTable = () => {
<CardTitle>
<div className="flex items-center">
<div className="flex-1 text-xl font-medium text-default-900">
{t("table", { defaultValue: "Table" })} {t("blog", { defaultValue: "Blog" })}
{t("table", { defaultValue: "Table" })}{" "}
{t("blog", { defaultValue: "Blog" })}
</div>
<div className="flex-none">
<Link href={"/contributor/blog/create"}>
@ -254,7 +254,9 @@ const BlogTable = () => {
<div className="flex flex-row justify-between my-1 mx-1">
<p>Filter</p>
</div>
<Label className="ml-2">{t("category", { defaultValue: "Category" })}</Label>
<Label className="ml-2">
{t("category", { defaultValue: "Category" })}
</Label>
{categories.length > 0 ? (
categories.map((category) => (
<div

View File

@ -7,18 +7,34 @@ import { Icon } from "@iconify/react/dist/iconify.js";
import NewContent from "@/components/landing-page/new-content";
import { useToast } from "@/components/ui/use-toast";
import { getCookiesDecrypt } from "@/lib/utils";
import { close, error, loading } from "@/config/swal";
import { checkWishlistStatus, deleteWishlist, getDetail, saveWishlist } from "@/service/landing/landing";
import { close, error, loading, successCallback } from "@/config/swal";
import {
checkWishlistStatus,
deleteWishlist,
getDetail,
getPublicSuggestionList,
saveWishlist,
} from "@/service/landing/landing";
import { Link, useRouter } from "@/i18n/routing";
import { postActivityLog } from "@/service/content/content";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { useTranslations } from "next-intl";
import { sendMediaUploadToEmail } from "@/service/media-tracking/media-tracking";
const DetailInfo = () => {
const [selectedSize, setSelectedSize] = useState<string>("L");
const [selectedTab, setSelectedTab] = useState("video");
const [emailShareList, setEmailShareList] = useState<any>();
const router = useRouter();
const pathname = usePathname();
const params = useParams();
const slug = String(params?.slug);
const t = useTranslations("LandingPage");
const [detailDataImage, setDetailDataImage] = useState<any>();
const [selectedImage, setSelectedImage] = useState(0);
const [isSaved, setIsSaved] = useState(false);
@ -30,8 +46,14 @@ const DetailInfo = () => {
const [main, setMain] = useState<any>();
const [resolutionSelected, setResolutionSelected] = useState("720");
const [imageSizeSelected, setImageSizeSelected] = useState("l");
const [emailShareInput, setEmailShareInput] = useState<any>();
const [emailMessageInput, setEmailMessageInput] = useState();
const [content, setContent] = useState<any>([]);
const [listSuggestion, setListSuggestion] = useState<any>();
const [width, setWidth] = useState<any>();
const userRoleId = getCookiesDecrypt("urie");
const userId = getCookiesDecrypt("uie");
let typeString = "image";
useEffect(() => {
initFetch();
@ -42,7 +64,12 @@ const DetailInfo = () => {
const initFetch = async () => {
const response = await getDetail(String(slug));
console.log("detailImage", response);
const responseGet = await getPublicSuggestionList(slug?.split("-")?.[0]);
setIsFromSPIT(response?.data?.data?.isFromSPIT);
setWidth(window.innerWidth);
setContent(response?.data.data);
setListSuggestion(responseGet?.data?.data);
setMain({
id: response?.data?.data?.files[0]?.id,
type: response?.data?.data?.fileType.name,
@ -63,6 +90,42 @@ const DetailInfo = () => {
setDetailDataImage(response?.data?.data);
};
const handleEmailList = (e: any) => {
const arrayEmail: any = [];
for (let i = 0; i < emailShareList?.length; i += 1) {
arrayEmail.push(emailShareList[i]);
}
if (e.which == 13) {
if (e.target.value) {
arrayEmail.push(e.target.value);
setEmailShareList(arrayEmail);
setEmailShareInput("");
}
e.preventDefault();
}
return false;
};
async function shareToEmail() {
if (Number(userRoleId) < 1 || userRoleId == undefined) {
router.push("/auth/login");
} else {
const data = {
mediaUploadId: slug?.split("-")?.[0],
email: emailShareList || [emailShareInput],
message: emailMessageInput,
url: window.location.href,
};
loading();
const res = await sendMediaUploadToEmail(data);
if (res?.error) {
error(res.message);
return false;
}
close();
successCallback("Konten Telah Dikirim");
}
}
const doBookmark = async () => {
if (userId) {
const data = {
@ -114,6 +177,26 @@ const DetailInfo = () => {
}
};
const handleShare = (type: any, url: any) => {
if (Number(userRoleId) < 1 || userRoleId == undefined) {
router.push("/auth/login");
} else {
sendActivityLog(2);
sendActivityLog(4);
if (type == "wa" && width <= 768) {
window.open(`whatsapp://send?${url}`, "_blank");
} else if (type == "wa" && width > 768) {
window.open(
`https://web.whatsapp.com/send?${url}`,
"_blank",
"noreferrer"
);
} else {
window.open(url);
}
}
};
const sizes = [
{ label: "XL", value: "3198 x 1798 px" },
{ label: "L", value: "2399 x 1349 px" },
@ -136,7 +219,7 @@ const DetailInfo = () => {
const handleDownload = () => {
if (downloadProgress === 0) {
if (!userId) {
router.push("/auth/login");
router.push("/auth");
} else {
sendActivityLog(2);
sendActivityLog(3);
@ -196,7 +279,8 @@ const DetailInfo = () => {
xhr.addEventListener("readystatechange", () => {
if (xhr.readyState === 4 && xhr.status === 200) {
const contentType = xhr.getResponseHeader("content-type") || "application/octet-stream";
const contentType =
xhr.getResponseHeader("content-type") || "application/octet-stream";
const extension = contentType.split("/")[1];
const filename = `${name}.${extension}`;
@ -229,7 +313,11 @@ const DetailInfo = () => {
<div className="md:w-3/4">
{/* Gambar Besar */}
<div className="relative">
<img src={detailDataImage?.files[selectedImage]?.url} alt="Main" className="rounded-lg w-auto h-fit" />
<img
src={detailDataImage?.files[selectedImage]?.url}
alt="Main"
className="rounded-lg w-auto h-fit"
/>
<div className="absolute top-4 left-4"></div>
</div>
@ -237,7 +325,10 @@ const DetailInfo = () => {
<div className="py-4 flex flex-row gap-3">
{detailDataImage?.files?.map((file: any, index: number) => (
<a onClick={() => setSelectedImage(index)} key={file?.id}>
<img src={file?.url} className="w-[120px] h-[90px] object-cover rounded-md cursor-pointer hover:ring-2 hover:ring-red-600" />
<img
src={file?.url}
className="w-[120px] h-[90px] object-cover rounded-md cursor-pointer hover:ring-2 hover:ring-red-600"
/>
</a>
))}
</div>
@ -245,65 +336,109 @@ const DetailInfo = () => {
{/* Footer Informasi */}
<div className="text-sm text-gray-500 flex justify-between items-center border-t mt-4">
<div className="flex flex-row items-center mt-3 justify-between">
oleh&nbsp;<span className="font-semibold text-black">{detailDataImage?.uploadedBy?.userLevel?.name}</span>&nbsp; | &nbsp;Diupdate pada {detailDataImage?.updatedAt} WIB &nbsp;|&nbsp;
oleh&nbsp;
<span className="font-semibold text-black">
{detailDataImage?.uploadedBy?.userLevel?.name}
</span>
&nbsp; | &nbsp;Diupdate pada {detailDataImage?.updatedAt} WIB
&nbsp;|&nbsp;
<Icon icon="formkit:eye" width="15" height="15" />
&nbsp; {detailDataImage?.clickCount} &nbsp;
<p className="flex text-end">Kreator: {detailDataImage?.creatorName}</p>
<p className="flex text-end">
Kreator: {detailDataImage?.creatorName}
</p>
</div>
</div>
{/* Keterangan */}
<div className="w-full">
<h1 className="flex flex-row font-bold text-2xl my-8">{detailDataImage?.title}</h1>
<div className="font-light text-justify" dangerouslySetInnerHTML={{ __html: detailDataImage?.htmlDescription }} />
<h1 className="flex flex-row font-bold text-2xl my-8">
{detailDataImage?.title}
</h1>
<div
className="font-light text-justify"
dangerouslySetInnerHTML={{
__html: detailDataImage?.htmlDescription,
}}
/>
</div>
</div>
{/* Bagian Kanan */}
<div className="md:w-1/4 p-4 bg-[#f7f7f7] h-fit rounded-lg mx-4">
<div className="p-4 bg-[#f7f7f7] dark:bg-slate-600 h-fit rounded-lg mx-4">
{isSaved ? (
<a onClick={() => handleDeleteWishlist()} className="flex flex-col mb-3 items-center justify-center cursor-pointer">
<a
onClick={() => handleDeleteWishlist()}
className="flex flex-col mb-3 items-center justify-center cursor-pointer"
>
<Icon icon="material-symbols:bookmark" width={40} />
<p className="text-base lg:text-lg">Hapus</p>
<p className="text-base lg:text-lg">
{t("delete", { defaultValue: "Delete" })}
</p>
</a>
) : (
<a onClick={() => doBookmark()} className="flex flex-col mb-3 items-center justify-center cursor-pointer">
<Icon icon="material-symbols:bookmark-outline" width={40} />
<p className="text-base lg:text-lg">Simpan</p>
<a
onClick={() => doBookmark()}
className="flex flex-col mb-3 items-center justify-center cursor-pointer"
>
<Icon icon="material-symbols:bookmark-outline" width={25} />
<p className="text-base lg:text-sm">
{t("save", { defaultValue: "Save" })}
</p>
</a>
)}
{/* garis */}
<div className="border-t border-black my-4"></div>
<Link href={`/all/filter?title=polda&category=${detailDataImage?.category.id}`} className="bg-red-600 text-white text-xs font-bold px-3 py-3 my-3 flex justify-center items-center rounded">
<div className="flex flex-col justify-center items-center gap-3">
<div className="w-auto mb-3">
<Link
href={`/all/filter?title=polda&category=${detailDataImage?.category.id}`}
className="bg-red-600 text-white text-xs font-bold px-3 py-3 my-3 rounded w-auto"
>
{detailDataImage?.category?.name}
</Link>
</div>
<div className="flex justify-center flex-wrap gap-2 mb-4">
{detailDataImage?.tags?.split(",").map((tag: string) => (
<a onClick={() => router.push(`/all/filter?tag=${tag}`)} key={tag} className="bg-gray-200 text-gray-700 text-xs px-3 py-1 rounded-full cursor-pointer hover:bg-gray-500">
<a
onClick={() => router.push(`/all/filter?tag=${tag}`)}
key={tag}
className="bg-gray-200 text-gray-700 text-xs px-3 py-3 font-semibold rounded-full cursor-pointer hover:bg-gray-500"
>
{tag}
</a>
))}
</div>
</div>
<div className="border-t border-black my-4"></div>
{/* Opsi Ukuran Foto */}
<h4 className="flex text-lg justify-center items-center font-semibold my-3">Opsi Ukuran Foto</h4>
<h4 className="flex text-lg justify-center items-center font-semibold my-3">
{t("imageSize", { defaultValue: "Image Size" })}
</h4>
<div className="border-t border-black my-4"></div>
<div className="space-y-2">
{sizes.map((size: any) => (
<div className="flex flex-row justify-between">
<div key={size.label} className="items-center flex flex-row gap-2 cursor-pointer">
<input type="radio" name="size" value={size.label} checked={selectedSize === size.label} onChange={() => setSelectedSize(size.label)} className="text-red-600 focus:ring-red-600" />
<div className="text-sm">{size.label}</div>
</div>
<div className="">
<div className="text-sm">{size.value}</div>
<div
key={size?.label}
className="items-center flex flex-row gap-2 cursor-pointer"
>
<Input
type="radio"
name="size"
value={size?.label}
checked={selectedSize === size?.label}
onChange={(e) => setImageSizeSelected(e.target.value)}
className="text-red-600 focus:ring-red-600"
/>
<div className="text-sm">{size?.label}</div>
</div>
<div className="text-sm">{size?.value}</div>
</div>
))}
</div>
@ -311,18 +446,177 @@ const DetailInfo = () => {
{/* Download Semua */}
<div className="mt-4">
<label className="flex items-center space-x-2 text-sm">
<input type="checkbox" className="text-red-600 focus:ring-red-600" onChange={() => setIsDownloadAll(!isDownloadAll)} />
<span>Download Semua File?</span>
<input
type="checkbox"
className="text-red-600 focus:ring-red-600"
onChange={() => setIsDownloadAll(!isDownloadAll)}
/>
<span>
{t("downloadAll", { defaultValue: "Download All" })}
</span>
</label>
</div>
{/* Tombol Download */}
<button onClick={handleDownload} className="mt-4 bg-red-600 text-white w-full py-2 flex justify-center items-center gap-1 rounded-md text-sm hover:bg-red-700">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path fill="white" d="m12 16l-5-5l1.4-1.45l2.6 2.6V4h2v8.15l2.6-2.6L17 11zm-6 4q-.825 0-1.412-.587T4 18v-3h2v3h12v-3h2v3q0 .825-.587 1.413T18 20z" />
<button
onClick={handleDownload}
className="mt-4 bg-red-600 text-white w-full py-2 flex justify-center items-center gap-1 rounded-md text-sm hover:bg-red-700"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
>
<path
fill="white"
d="m12 16l-5-5l1.4-1.45l2.6 2.6V4h2v8.15l2.6-2.6L17 11zm-6 4q-.825 0-1.412-.587T4 18v-3h2v3h12v-3h2v3q0 .825-.587 1.413T18 20z"
/>
</svg>
Download
{t("download", { defaultValue: "Download" })}
</button>
{/* Tombol Bagikan */}
<div className="flex flex-row mt-5 justify-center">
<div className="flex items-center gap-1 hover:text-red-800 w-full rounded-lg">
<Popover>
<PopoverTrigger asChild>
<button className="w-full flex items-center gap-2">
{/* <Icon icon="oi:share" fontSize={20} /> */}
<p className="text-base font-semibold mb-2">
{t("share", { defaultValue: "Share" })}
</p>
</button>
</PopoverTrigger>
<PopoverContent>
<div className="flex flex-col">
<h1 className="mb-2">
{t("shareTo", { defaultValue: "Share To" })}
</h1>
<div className="flex flex-col mb-2">
<p className="text-base font-semibold mb-1">
{t("destinationEmail", {
defaultValue: "Destination Email",
})}
</p>
<Input
value={emailShareInput}
onChange={(event) =>
setEmailShareInput(event.target.value)
}
onKeyPress={handleEmailList}
type="email"
placeholder={t("pressEnter", {
defaultValue: "Press Enter",
})}
/>
</div>
<Button
className="bg-blue-500 text-white p-2 w-fit rounded-lg"
onClick={() => shareToEmail()}
>
{t("send", { defaultValue: "Send" })}
</Button>
</div>
</PopoverContent>
</Popover>
</div>
<a
className="ml-8 cursor-pointer"
onClick={() =>
handleShare(
"fb",
`https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fmediahub.polri.go.id%2F${typeString}%2Fdetail%2F${content?.id}&quote=${content?.title}`
)
}
>
<Icon
icon="brandico:facebook"
height="20"
className="px-auto text-red-600 text-center"
/>
</a>
<a
className="ml-5 cursor-pointer"
onClick={() =>
handleShare(
"tw",
`https://twitter.com/share?url=https%3A%2F%2Fmediahub.polri.go.id%2F${typeString}%2Fdetail%2F${content?.id}&text=${content?.title}`
)
}
>
<Icon
icon="mdi:twitter"
width="23"
className="text-red-600 text-center"
/>
</a>
<a
className="ml-5 cursor-pointer"
onClick={() =>
handleShare(
"wa",
`text=${content?.title}%0D%0A%0D%0Ahttps%3A%2F%2Fmediahub.polri.go.id%2F${typeString}%2Fdetail%2F${content?.id}`
)
}
>
<Icon
icon="ri:whatsapp-fill"
width="23"
className="text-red-600 text-center"
/>
</a>
<Popover>
<PopoverTrigger
className="flex justify-end gap-1 cursor-pointer"
asChild
>
<a
className="ml-5 cursor-pointer"
data-toggle="dropdown"
href="#"
aria-expanded="false"
>
<Icon
icon="material-symbols-light:mail"
width="23"
className="text-red-600 text-center"
/>
</a>
</PopoverTrigger>
<PopoverContent>
<div className="flex flex-col">
<h1 className="mb-2">
{t("shareTo", { defaultValue: "Share To" })}
</h1>
<div className="flex flex-col mb-2">
<p className="text-base font-semibold mb-1">
{t("destinationEmail", {
defaultValue: "Destination Email",
})}
</p>
<Input
value={emailShareInput}
onChange={(event) =>
setEmailShareInput(event.target.value)
}
onKeyPress={handleEmailList}
type="email"
placeholder={t("pressEnter", {
defaultValue: "Press Enter",
})}
/>
</div>
<Button
className="bg-blue-500 text-white p-2 w-fit rounded-lg"
onClick={() => shareToEmail()}
>
{t("send", { defaultValue: "Send" })}
</Button>
</div>
</PopoverContent>
</Popover>
</div>
</div>
</div>
</div>
@ -332,13 +626,18 @@ const DetailInfo = () => {
<div className="flex flex-col my-16 p-10 bg-[#f7f7f7]">
<div className="gap-5 flex flex-col px-4 lg:px-14">
<p className="flex items-start text-lg">Berikan Komentar</p>
<Textarea placeholder="Type your comments here." className="flex w-full" />
<button className="flex items-start bg-[#bb3523] rounded-lg w-fit text-white px-4 py-1">Kirim</button>
<Textarea
placeholder="Type your comments here."
className="flex w-full"
/>
<button className="flex items-start bg-[#bb3523] rounded-lg w-fit text-white px-4 py-1">
Kirim
</button>
</div>
</div>
{/* Konten Serupa */}
<div className="">
<div className="px-4 lg:px-24">
<NewContent group="satker" type={"similar"} />
</div>
</div>

View File

@ -1,17 +1,44 @@
"use client";
import { useParams, usePathname } from "next/navigation";
import { useParams, usePathname, useSearchParams } from "next/navigation";
import React, { useEffect, useState } from "react";
import { Icon } from "@iconify/react/dist/iconify.js";
import { checkWishlistStatus, deleteWishlist, getDetail, saveWishlist } from "@/service/landing/landing";
import {
checkWishlistStatus,
createPublicSuggestion,
deletePublicSuggestion,
deleteWishlist,
getDetail,
getPublicSuggestionList,
saveWishlist,
} from "@/service/landing/landing";
import VideoPlayer from "@/utils/video-player";
import NewContent from "@/components/landing-page/new-content";
import { Link, useRouter } from "@/i18n/routing";
import { Textarea } from "@/components/ui/textarea";
import { getCookiesDecrypt } from "@/lib/utils";
import { close, error, loading } from "@/config/swal";
import { close, error, loading, successCallback, warning } from "@/config/swal";
import { useToast } from "@/components/ui/use-toast";
import { postActivityLog } from "@/service/content/content";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { useTranslations } from "next-intl";
import { sendMediaUploadToEmail } from "@/service/media-tracking/media-tracking";
import {
checkMaliciousText,
getPublicLocaleTimestamp,
toBase64,
} from "@/utils/globals";
import Image from "next/image";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
import parse from "html-react-parser";
const DetailVideo = () => {
const [selectedSize, setSelectedSize] = useState<string>("L");
@ -20,6 +47,7 @@ const DetailVideo = () => {
const pathname = usePathname();
const params = useParams();
const slug = String(params?.slug);
const searchParams = useSearchParams();
const [detailDataVideo, setDetailDataVideo] = useState<any>();
const [isSaved, setIsSaved] = useState(false);
const [wishlistId, setWishlistId] = useState();
@ -28,9 +56,21 @@ const DetailVideo = () => {
const [downloadProgress, setDownloadProgress] = useState(0);
const [isFromSPIT, setIsFromSPIT] = useState(false);
const [main, setMain] = useState<any>();
const [emailMessageInput, setEmailMessageInput] = useState();
const [resolutionSelected, setResolutionSelected] = useState("720");
const t = useTranslations("LandingPage");
const [emailShareInput, setEmailShareInput] = useState<any>();
const [emailShareList, setEmailShareList] = useState<any>();
const userRoleId = getCookiesDecrypt("urie");
const userId = getCookiesDecrypt("uie");
const id = searchParams?.get("id");
const [width, setWidth] = useState<any>();
const [content, setContent] = useState<any>([]);
const [listSuggestion, setListSuggestion] = useState<any>();
const [visibleInput, setVisibleInput] = useState(null);
let typeString = "video";
const MySwal = withReactContent(Swal);
const [message, setMessage] = useState("");
useEffect(() => {
initFetch();
@ -41,7 +81,12 @@ const DetailVideo = () => {
const initFetch = async () => {
const response = await getDetail(String(slug));
console.log("detailVideo", response);
const responseGet = await getPublicSuggestionList(slug?.split("-")?.[0]);
setIsFromSPIT(response?.data?.data?.isFromSPIT);
setWidth(window.innerWidth);
setContent(response?.data.data);
setListSuggestion(responseGet?.data?.data);
setMain({
id: response?.data?.data?.files[0]?.id,
type: response?.data?.data?.fileType.name,
@ -62,6 +107,40 @@ const DetailVideo = () => {
setDetailDataVideo(response?.data?.data);
};
async function sendSuggestionParent() {
if (message?.length > 3) {
loading();
const data = {
mediaUploadId: slug?.split("-")?.[0],
message,
parentId: null,
};
const response = await createPublicSuggestion(data);
console.log(response);
setMessage("");
const responseGet = await getPublicSuggestionList(slug?.split("-")?.[0]);
console.log(responseGet?.data?.data);
setListSuggestion(responseGet?.data?.data);
// Hapus nilai semua input secara manual jika perlu
const inputs = document.querySelectorAll("input");
inputs.forEach((input) => {
input.value = "";
});
close();
}
}
const getInputValue = (e: any) => {
const message = e.target.value;
console.log(message);
setMessage(message);
};
const doBookmark = async () => {
if (userId) {
const data = {
@ -83,6 +162,94 @@ const DetailVideo = () => {
router.push("/auth");
}
};
async function sendSuggestionChild(parentId: any) {
const inputElement = document.querySelector(
`#input-comment-${parentId}`
) as HTMLInputElement;
if (inputElement && inputElement.value.length > 3) {
loading();
const data = {
mediaUploadId: slug?.split("-")?.[0],
message: inputElement.value,
parentId,
};
console.log(data);
const response = await createPublicSuggestion(data);
console.log(response);
const responseGet: any = await getPublicSuggestionList(
slug?.split("-")?.[0]
);
console.log(responseGet?.data?.data);
setListSuggestion(responseGet?.data?.data);
// Reset input field
inputElement.value = "";
// document.querySelector("#comment-id-" + parentId)?.style.display = "none";
close();
}
}
async function deleteDataSuggestion(dataId: any) {
loading();
const response = await deletePublicSuggestion(dataId);
console.log(response);
const responseGet = await getPublicSuggestionList(slug.split("-")?.[0]);
console.log(responseGet?.data?.data);
setListSuggestion(responseGet?.data?.data);
close();
}
const postDataChild = (id: any) => {
const checkMessage = checkMaliciousText(message);
if (checkMessage == "") {
if (Number(userRoleId) < 1 || userRoleId == undefined) {
router.push("/auth");
} else {
sendSuggestionChild(id);
}
} else {
warning(checkMessage);
}
};
const handleShare = (type: any, url: any) => {
if (Number(userRoleId) < 1 || userRoleId == undefined) {
router.push("/auth/login");
} else {
sendActivityLog(2);
sendActivityLog(4);
if (type == "wa" && width <= 768) {
window.open(`whatsapp://send?${url}`, "_blank");
} else if (type == "wa" && width > 768) {
window.open(
`https://web.whatsapp.com/send?${url}`,
"_blank",
"noreferrer"
);
} else {
window.open(url);
}
}
};
const postData = () => {
const checkMessage = checkMaliciousText(message);
if (checkMessage == "") {
if (Number(userRoleId) < 1 || userRoleId == undefined) {
router.push("/auth");
} else {
sendSuggestionParent();
}
} else {
warning(checkMessage);
}
};
async function checkWishlist() {
if (userId) {
const res = await checkWishlistStatus(slug.split("-")?.[0]);
@ -177,6 +344,28 @@ const DetailVideo = () => {
}
};
const deleteData = (dataId: any) => {
MySwal.fire({
title: "Delete Comment",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#3085d6",
confirmButtonColor: "#d33",
confirmButtonText: "Delete",
}).then((result: any) => {
if (result.isConfirmed) {
deleteDataSuggestion(dataId);
console.log(dataId);
}
});
};
const showInput = (e: any) => {
console.log(document.querySelector(`#${e}`)?.classList);
document.querySelector(`#${e}`)?.classList.toggle("none");
setVisibleInput(visibleInput === e ? null : e);
};
const downloadFile = (fileUrl: string, name: string) => {
const xhr = new XMLHttpRequest();
@ -192,7 +381,8 @@ const DetailVideo = () => {
xhr.addEventListener("readystatechange", () => {
if (xhr.readyState === 4 && xhr.status === 200) {
const contentType = xhr.getResponseHeader("content-type") || "application/octet-stream";
const contentType =
xhr.getResponseHeader("content-type") || "application/octet-stream";
const extension = contentType.split("/")[1];
const filename = `${name}.${extension}`;
@ -217,6 +407,61 @@ const DetailVideo = () => {
xhr.send();
};
async function shareToEmail() {
if (Number(userRoleId) < 1 || userRoleId == undefined) {
router.push("/auth/login");
} else {
const data = {
mediaUploadId: id?.split("-")?.[0],
email: emailShareList || [emailShareInput],
message: emailMessageInput,
url: window.location.href,
};
loading();
const res = await sendMediaUploadToEmail(data);
if (res?.error) {
error(res.message);
return false;
}
close();
successCallback("Konten Telah Dikirim");
}
}
const handleEmailList = (e: any) => {
const arrayEmail: any = [];
for (let i = 0; i < emailShareList?.length; i += 1) {
arrayEmail.push(emailShareList[i]);
}
if (e.which == 13) {
if (e.target.value) {
arrayEmail.push(e.target.value);
setEmailShareList(arrayEmail);
setEmailShareInput("");
}
e.preventDefault();
}
return false;
};
function addDefaultProfile(ev: any) {
ev.target.src = "/assets/avatar-profile.png";
}
const shimmer = (w: number, h: number) => `
<svg width="${w}" height="${h}" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient id="g">
<stop stop-color="#bcbcbd" offset="20%" />
<stop stop-color="#f9fafb" offset="50%" />
<stop stop-color="#bcbcbd" offset="70%" />
</linearGradient>
</defs>
<rect width="${w}" height="${h}" fill="#bcbcbd" />
<rect id="r" width="${w}" height="${h}" fill="url(#g)" />
<animate xlink:href="#r" attributeName="x" from="-${w}" to="${w}" dur="1s" repeatCount="indefinite" />
</svg>`;
return (
<>
<div className="px-4 md:px-24 py-4">
@ -233,8 +478,11 @@ const DetailVideo = () => {
<div className="text-sm text-gray-500 flex justify-between items-center border-t mt-4">
<p className="flex flex-row items-center mt-3">
oleh&nbsp;
<span className="font-semibold text-black">{detailDataVideo?.uploadedBy?.userLevel?.name}</span>
&nbsp;|&nbsp;Diupdate pada {detailDataVideo?.updatedAt} WIB&nbsp;|&nbsp;
<span className="font-semibold text-black">
{detailDataVideo?.uploadedBy?.userLevel?.name}
</span>
&nbsp;|&nbsp;Diupdate pada {detailDataVideo?.updatedAt}{" "}
WIB&nbsp;|&nbsp;
<Icon icon="formkit:eye" width="15" height="15" />
&nbsp;
{detailDataVideo?.clickCount}
@ -244,7 +492,9 @@ const DetailVideo = () => {
{/* Keterangan */}
<div className="w-full">
<h1 className="flex flex-row font-bold text-2xl my-8">{detailDataVideo?.title}</h1>
<h1 className="flex flex-row font-bold text-2xl my-8">
{detailDataVideo?.title}
</h1>
<div
className="font-light text-justify"
dangerouslySetInnerHTML={{
@ -255,29 +505,46 @@ const DetailVideo = () => {
</div>
{/* Bagian Kanan */}
<div className="md:w-1/4 p-4 bg-[#f7f7f7] rounded-lg mx-4 h-fit">
<div className="md:w-1/4 p-4 bg-[#f7f7f7] dark:bg-slate-600 rounded-lg mx-4 h-fit">
{isSaved ? (
<a onClick={() => handleDeleteWishlist()} className="flex flex-col mb-3 items-center justify-center cursor-pointer">
<a
onClick={() => handleDeleteWishlist()}
className="flex flex-col mb-3 items-center justify-center cursor-pointer"
>
<Icon icon="material-symbols:bookmark" width={40} />
<p className="text-base lg:text-lg">Hapus</p>
<p className="text-base lg:text-lg">
{t("delete", { defaultValue: "Delete" })}
</p>
</a>
) : (
<a onClick={() => doBookmark()} className="flex flex-col mb-3 items-center justify-center cursor-pointer">
<a
onClick={() => doBookmark()}
className="flex flex-col mb-3 items-center justify-center cursor-pointer"
>
<Icon icon="material-symbols:bookmark-outline" width={40} />
<p className="text-base lg:text-lg">Simpan</p>
<p className="text-base lg:text-lg">
{t("save", { defaultValue: "Save" })}
</p>
</a>
)}
{/* garis */}
<div className="border-t border-black my-4"></div>
<Link href={`/all/filter?title=polda&category=${detailDataVideo?.category.id}`} className="bg-red-600 text-white text-xs font-bold px-3 py-3 my-3 flex justify-center items-center rounded">
<Link
href={`/all/filter?title=polda&category=${detailDataVideo?.category.id}`}
className="bg-red-600 text-white text-xs font-bold px-3 py-3 my-3 flex justify-center items-center rounded"
>
{detailDataVideo?.categoryName}
</Link>
<div className="flex justify-center flex-wrap gap-2 mb-4">
{detailDataVideo?.tags.split(",").map((tag: string) => (
<a onClick={() => router.push(`/all/filter?tag=${tag}`)} key={tag} className="bg-gray-200 text-gray-700 text-xs px-3 py-1 rounded-full cursor-pointer hover:bg-gray-500 hover:text-white">
<a
onClick={() => router.push(`/all/filter?tag=${tag}`)}
key={tag}
className="bg-gray-200 text-gray-700 text-xs px-3 py-1 rounded-full cursor-pointer hover:bg-gray-500 hover:text-white"
>
{tag}
</a>
))}
@ -286,15 +553,27 @@ const DetailVideo = () => {
<div className="border-t border-black my-4"></div>
{/* Opsi Ukuran Foto */}
<h4 className="flex text-lg justify-center items-center font-semibold my-3">Opsi Ukuran Audio Visual</h4>
<h4 className="flex text-lg justify-center items-center font-semibold my-3">
{t("videoSize", { defaultValue: "Video Size" })}
</h4>
<div className="border-t border-black my-4"></div>
<div className="space-y-2">
{sizes.map((size: any) => (
<div className="flex flex-row justify-between">
<div key={size.label} className="items-center flex flex-row gap-2 cursor-pointer">
<input type="radio" name="size" value={size.label} checked={selectedSize === size.label} onChange={() => setSelectedSize(size.label)} className="text-red-600 focus:ring-red-600" />
<div
key={size.label}
className="items-center flex flex-row gap-2 cursor-pointer"
>
<input
type="radio"
name="size"
value={size.label}
checked={selectedSize === size.label}
onChange={() => setSelectedSize(size.label)}
className="text-red-600 focus:ring-red-600"
/>
<div className="text-sm">{size.label}</div>
</div>
<div className="">
@ -307,33 +586,511 @@ const DetailVideo = () => {
{/* Download Semua */}
<div className="mt-4">
<label className="flex items-center space-x-2 text-sm">
<input type="checkbox" className="text-red-600 focus:ring-red-600" onChange={() => setIsDownloadAll(!isDownloadAll)} />
<span>Download Semua File?</span>
<input
type="checkbox"
className="text-red-600 focus:ring-red-600"
onChange={() => setIsDownloadAll(!isDownloadAll)}
/>
<span>
{t("downloadAll", { defaultValue: "Download All" })}
</span>
</label>
</div>
{/* Tombol Download */}
<button onClick={handleDownload} className="mt-4 bg-red-600 text-white w-full py-2 flex justify-center items-center gap-1 rounded-md text-sm hover:bg-red-700">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path fill="white" d="m12 16l-5-5l1.4-1.45l2.6 2.6V4h2v8.15l2.6-2.6L17 11zm-6 4q-.825 0-1.412-.587T4 18v-3h2v3h12v-3h2v3q0 .825-.587 1.413T18 20z" />
<button
onClick={handleDownload}
className="mt-4 bg-red-600 text-white w-full py-2 flex justify-center items-center gap-1 rounded-md text-sm hover:bg-red-700"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
>
<path
fill="white"
d="m12 16l-5-5l1.4-1.45l2.6 2.6V4h2v8.15l2.6-2.6L17 11zm-6 4q-.825 0-1.412-.587T4 18v-3h2v3h12v-3h2v3q0 .825-.587 1.413T18 20z"
/>
</svg>
Download
{t("download", { defaultValue: "Download" })}
</button>
{/* Tombol Bagikan */}
<div className="flex flex-row justify-center py-3">
<div className="flex items-center gap-1 hover:text-red-800 w-full rounded-lg">
<Popover>
<PopoverTrigger asChild>
<button className="w-full flex items-center gap-2">
{/* <Icon icon="oi:share" fontSize={20} /> */}
<p className="text-base font-semibold mb-2">
{t("share", { defaultValue: "Share" })}
</p>
</button>
</PopoverTrigger>
<PopoverContent>
<div className="flex flex-col">
<h1 className="mb-2">
{t("shareTo", { defaultValue: "Share To" })}
</h1>
<div className="flex flex-col mb-2">
<p className="text-base font-semibold mb-1">
{t("destinationEmail", {
defaultValue: "Destination Email",
})}
</p>
<Input
value={emailShareInput}
onChange={(event) =>
setEmailShareInput(event.target.value)
}
onKeyPress={handleEmailList}
type="email"
placeholder={t("pressEnter", {
defaultValue: "Press Enter",
})}
/>
</div>
<Button
className="bg-blue-500 text-white p-2 w-fit rounded-lg"
onClick={() => shareToEmail()}
>
{t("send", { defaultValue: "Send" })}
</Button>
</div>
</PopoverContent>
</Popover>
</div>
<a
className="ml-8 cursor-pointer"
onClick={() =>
handleShare(
"fb",
`https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fmediahub.polri.go.id%2F${typeString}%2Fdetail%2F${content?.id}&quote=${content?.title}`
)
}
>
<Icon
icon="brandico:facebook"
height="20"
className="px-auto text-red-600 text-center"
/>
</a>
<a
className="ml-5 cursor-pointer"
onClick={() =>
handleShare(
"tw",
`https://twitter.com/share?url=https%3A%2F%2Fmediahub.polri.go.id%2F${typeString}%2Fdetail%2F${content?.id}&text=${content?.title}`
)
}
>
<Icon
icon="mdi:twitter"
width="23"
className="text-red-600 text-center"
/>
</a>
<a
className="ml-5 cursor-pointer"
onClick={() =>
handleShare(
"wa",
`text=${content?.title}%0D%0A%0D%0Ahttps%3A%2F%2Fmediahub.polri.go.id%2F${typeString}%2Fdetail%2F${content?.id}`
)
}
>
<Icon
icon="ri:whatsapp-fill"
width="23"
className="text-red-600 text-center"
/>
</a>
<Popover>
<PopoverTrigger
className="flex justify-end gap-1 cursor-pointer"
asChild
>
<a
className="ml-5 cursor-pointer"
data-toggle="dropdown"
href="#"
aria-expanded="false"
>
<Icon
icon="material-symbols-light:mail"
width="23"
className="text-red-600 text-center"
/>
</a>
</PopoverTrigger>
<PopoverContent>
<div className="flex flex-col">
<h1 className="mb-2">
{t("shareTo", { defaultValue: "Share To" })}
</h1>
<div className="flex flex-col mb-2">
<p className="text-base font-semibold mb-1">
{t("destinationEmail", {
defaultValue: "Destination Email",
})}
</p>
<Input
value={emailShareInput}
onChange={(event) =>
setEmailShareInput(event.target.value)
}
onKeyPress={handleEmailList}
type="email"
placeholder={t("pressEnter", {
defaultValue: "Press Enter",
})}
/>
</div>
<Button
className="bg-blue-500 text-white p-2 w-fit rounded-lg"
onClick={() => shareToEmail()}
>
{t("send", { defaultValue: "Send" })}
</Button>
</div>
</PopoverContent>
</Popover>
</div>
</div>
</div>
</div>
<div className="w-full mb-8">
{/* Comment */}
<div className="flex flex-col my-16 p-10 bg-[#f7f7f7]">
<div className="flex flex-col my-16 p-4 lg:p-10 bg-[#f7f7f7] dark:bg-slate-600">
<div className="gap-5 flex flex-col px-4 lg:px-14">
<p className="flex items-start text-lg">Berikan Komentar</p>
<Textarea placeholder="Type your comments here." className="flex w-full" />
<button className="flex items-start bg-[#bb3523] text-white rounded-lg w-fit px-4 py-1">Kirim</button>
<p className="flex items-start text-lg">
{t("comment", { defaultValue: "Comment" })}
</p>
<Textarea
placeholder="Type your comments here."
className="flex w-full pb-12"
onChange={getInputValue}
/>
<button
onClick={() => postData()}
className="flex items-start bg-[#bb3523] rounded-lg w-fit text-white px-4 py-1"
>
{t("send", { defaultValue: "Send" })}
</button>
</div>
<div className="border-b-2 border-slate-300 mt-4 w-auto"></div>
<div>
{listSuggestion?.map((data: any) => (
<div className="flex flex-col">
<div className="flex flex-row mt-2 px-4 lg:px-14">
<Image
placeholder={`data:image/svg+xml;base64,${toBase64(
shimmer(700, 475)
)}`}
width={1080}
height={1080}
src={data?.suggestionFrom?.profilePictureUrl}
className="h-12 lg:h-16 w-12 lg:w-16 mr-2"
onError={addDefaultProfile}
alt=""
/>
<div className="border border-slate-300 w-full p-2 lg:p-4 bg-white gap-1">
<p className="text-slate-500 text-sm lg:text-base border-b-2 border-slate-200 mb-2">
{Number(data.suggestionFrom?.roleId) == 2 ||
Number(data.suggestionFrom?.roleId) == 3 ||
Number(data.suggestionFrom?.roleId) == 4
? "HUMAS POLRI"
: data.suggestionFrom?.fullname}
{getPublicLocaleTimestamp(new Date(data.createdAt))}
</p>
<p className="text-slate-500 text-[13px] lg:text-sm mb-4">
{data?.message}
</p>
<div>
<a
style={
Number(data.suggestionFrom?.id) == Number(userId)
? {
display: "none",
}
: {}
}
onClick={() => showInput(`comment-id-${data.id}`)}
className="mr-2"
>
<small className="flex items-start bg-[#bb3523] rounded-lg w-fit text-white px-2 text-xs lg:text-base lg:px-4 py-1 cursor-pointer">
{t("reply", { defaultValue: "Reply" })}
</small>
</a>
{Number(data.suggestionFrom?.id) == Number(userId) ||
Number(userRoleId) == 2 ? (
<a onClick={() => deleteData(data.id)}>
<small className="flex items-start bg-[#bb3523] rounded-lg w-fit text-white px-2 text-xs lg:text-base lg:px-4 py-1 cursor-pointer">
{t("delete", { defaultValue: "Delete" })}
</small>
</a>
) : (
""
)}
</div>
</div>
</div>
{visibleInput === `comment-id-${data.id}` && (
<div
id={`comment-id-${data.id}`}
className="px-4 pl-[72px] lg:px-14 lg:pl-32 mt-2 "
>
<Textarea
id={`input-comment-${data.id}`}
className="p-4 focus:outline-none focus:border-sky-500"
placeholder={t("enterReply", {
defaultValue: "Enter Reply",
})}
/>
<div className="flex flex-row gap-3">
<a onClick={() => postDataChild(data.id)}>
<small className="flex items-start bg-[#bb3523] rounded-lg w-fit text-white px-2 text-xs lg:text-base lg:px-4 py-1 mt-2 cursor-pointer">
{t("send", { defaultValue: "Send" })}
</small>
</a>
<a onClick={() => showInput(`comment-id-${data.id}`)}>
<small className="flex items-start bg-[#bb3523] rounded-lg mt-2 w-fit text-white px-2 text-xs lg:text-base lg:px-4 py-1 cursor-pointer">
{t("cancel", { defaultValue: "Cancel" })}
</small>
</a>
</div>
</div>
)}
{data.children.length > 0
? data.children?.map((child1: any) => (
<div className="flex flex-col">
<div className="flex flex-row mt-2 px-4 lg:pr-14 pl-16 lg:pl-32">
<Image
placeholder={`data:image/svg+xml;base64,${toBase64(
shimmer(700, 475)
)}`}
width={1080}
height={1080}
src={child1.suggestionFrom?.profilePictureUrl}
onError={addDefaultProfile}
alt=""
className="h-10 lg:h-16 w-10 lg:w-16 mr-2"
/>
<div className="border border-slate-300 w-full p-2 lg:p-4 bg-white gap-1">
<p className="text-slate-500 text-sm lg:text-base border-b-2 border-slate-200 mb-2">
{" "}
<b>
{Number(child1.suggestionFrom?.roleId) == 2 ||
Number(child1.suggestionFrom?.roleId) == 3 ||
Number(child1.suggestionFrom?.roleId) == 4
? "HUMAS POLRI"
: child1.suggestionFrom?.fullname}
</b>{" "}
{getPublicLocaleTimestamp(
new Date(child1.createdAt)
)}
</p>
<p className="text-slate-500 text-[13px] lg:text-sm mb-4">
{parse(String(child1?.message))}
</p>
<div>
<a
style={
Number(child1.suggestionFrom?.id) ==
Number(userId)
? {
display: "none",
}
: {}
}
onClick={() =>
showInput(`comment-id-${child1.id}`)
}
>
<small className="flex items-start bg-[#bb3523] rounded-lg w-fit text-white px-2 text-xs lg:text-base lg:px-4 py-1 cursor-pointer">
{t("reply", { defaultValue: "Reply" })}
</small>
</a>
<a
style={
Number(child1.suggestionFrom?.id) ==
Number(userId)
? {}
: {
display: "none",
}
}
onClick={() => deleteData(child1.id)}
>
<small className="flex items-start bg-[#bb3523] rounded-lg w-fit text-white px-2 text-xs lg:text-base lg:px-4 py-1 cursor-pointer">
{t("delete", { defaultValue: "Delete" })}
</small>
</a>
</div>
</div>
</div>
{visibleInput === `comment-id-${child1.id}` && (
<div
id={`comment-id-${child1.id}`}
className="px-4 lg:px-14 pl-28 lg:pl-[200px]"
>
<Textarea
name=""
className="mt-2 "
id={`input-comment-${child1.id}`}
placeholder={t("enterReply", {
defaultValue: "Enter Reply",
})}
/>
<div className="flex flex-row mt-2 gap-3">
<a onClick={() => postDataChild(child1.id)}>
<small className="flex items-start bg-[#bb3523] rounded-lg w-fit text-white px-2 text-xs lg:text-base lg:px-4 py-1 cursor-pointer">
{t("send", { defaultValue: "Send" })}
</small>
</a>
<a
onClick={() =>
showInput(`comment-id-${child1.id}`)
}
>
<small className="flex items-start bg-[#bb3523] rounded-lg w-fit text-white px-2 text-xs lg:text-base lg:px-4 py-1 cursor-pointer">
{t("cancel", { defaultValue: "Cancel" })}
</small>
</a>
</div>
</div>
)}
{child1.children.length > 0
? child1.children?.map((child2: any) => (
<div className="">
<div className="flex flex-row mt-2 px-4 lg:pr-14 pl-28 lg:pl-48">
<Image
placeholder={`data:image/svg+xml;base64,${toBase64(
shimmer(700, 475)
)}`}
width={1080}
height={1080}
src={
child2.suggestionFrom?.profilePictureUrl
}
className="h-9 lg:h-16 w-9 lg:w-16 mr-2"
onError={addDefaultProfile}
alt=""
/>
<div className="border border-slate-300 w-full p-2 lg:p-4 bg-white gap-1">
<p className="text-slate-500 text-sm lg:text-base border-b-2 border-slate-200 mb-2">
{" "}
<b>
{Number(
child2.suggestionFrom?.roleId
) == 2 ||
Number(child2.suggestionFrom?.roleId) ==
3 ||
Number(child2.suggestionFrom?.roleId) ==
4
? "HUMAS POLRI"
: child2.suggestionFrom?.fullname}
</b>{" "}
{getPublicLocaleTimestamp(
new Date(child2.createdAt)
)}
</p>
<p className="text-slate-500 text-sm mb-4">
{parse(String(child2?.message))}
</p>
<div>
<a
style={
Number(child2.suggestionFrom?.id) ==
Number(userId)
? {
display: "none",
}
: {}
}
onClick={() =>
showInput(`comment-id-${child2.id}`)
}
>
<small className="flex items-start bg-[#bb3523] rounded-lg w-fit text-white px-2 text-xs lg:text-base lg:px-4 py-1 cursor-pointer">
{t("reply", {
defaultValue: "Reply",
})}
</small>
</a>
<a
style={
Number(child2.suggestionFrom?.id) ==
Number(userId)
? {}
: {
display: "none",
}
}
onClick={() => deleteData(child2.id)}
>
<small className="flex items-start bg-[#bb3523] rounded-lg w-fit text-white px-2 text-xs lg:text-base lg:px-4 py-1 cursor-pointer">
{t("delete", {
defaultValue: "Delete",
})}
</small>
</a>
</div>
</div>
</div>
{visibleInput === `comment-id-${child2.id}` && (
<div
id={`comment-id-${child2.id}`}
className="px-4 lg:px-14 pl-40 lg:pl-[265px]"
>
<Textarea
name=""
id={`input-comment-${child2.id}`}
className="my-2"
placeholder={t("enterReply", {
defaultValue: "Enter Reply",
})}
/>
<div className="flex flex-row gap-3">
<a
onClick={() => postDataChild(child2.id)}
>
<small className="flex items-start bg-[#bb3523] rounded-lg w-fit text-white px-2 text-xs lg:text-base lg:px-4 py-1 cursor-pointer">
{t("send", { defaultValue: "Send" })}
</small>
</a>
<a
onClick={() =>
showInput(`comment-id-${child2.id}`)
}
>
<small className="flex items-start bg-[#bb3523] rounded-lg w-fit text-white px-2 text-xs lg:text-base lg:px-4 py-1 cursor-pointer">
{t("cancel", {
defaultValue: "Cancel",
})}
</small>
</a>
</div>
</div>
)}
</div>
))
: ""}
</div>
))
: ""}
</div>
))}
</div>
</div>
{/* Konten Serupa */}
<div className="">
<div className="px-4 lg:px-24">
<NewContent group="satker" type={"similar"} />
</div>
</div>

View File

@ -56,6 +56,7 @@ export default function FormCollaboration() {
const router = useRouter();
const editor = useRef(null);
type TaskSchema = z.infer<typeof taskSchema>;
const [isSubmitting, setIsSubmitting] = useState(false);
const [taskOutput, setTaskOutput] = useState({
all: false,
@ -162,6 +163,18 @@ export default function FormCollaboration() {
}
const save = async (data: TaskSchema) => {
setIsSubmitting(true);
MySwal.fire({
title: "Menyimpan...",
text: "Mohon tunggu sebentar",
allowOutsideClick: false,
allowEscapeKey: false,
didOpen: () => {
Swal.showLoading();
},
});
const requestData = {
title: data.title,
typeId: 11,
@ -179,6 +192,8 @@ export default function FormCollaboration() {
console.log("Form Data Submitted:", requestData);
console.log("response", response);
Swal.close();
MySwal.fire({
title: "Sukses",
text: "Data berhasil disimpan.",
@ -188,6 +203,8 @@ export default function FormCollaboration() {
}).then(() => {
router.push("/in/supervisor/communications/collaboration");
});
setIsSubmitting(false);
};
const onSubmit = (data: TaskSchema) => {

View File

@ -10,8 +10,6 @@ import * as z from "zod";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { useRouter } from "next/navigation";
import {
getCuratorUser,
getTicketingPriority,
@ -19,13 +17,6 @@ import {
} from "@/service/communication/communication";
import makeAnimated from "react-select/animated";
import Select from "react-select";
import {
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@radix-ui/react-select";
import { SelectGroup } from "@/components/ui/select";
import dynamic from "next/dynamic";
const taskSchema = z.object({
@ -45,39 +36,20 @@ interface Option {
}
const CustomEditor = dynamic(
() => {
return import("@/components/editor/custom-editor");
},
() => import("@/components/editor/custom-editor"),
{ ssr: false }
);
export default function FormInternal() {
const MySwal = withReactContent(Swal);
const router = useRouter();
const editor = useRef(null);
type TaskSchema = z.infer<typeof taskSchema>;
// State for various form fields
const [taskOutput, setTaskOutput] = useState({
all: false,
video: false,
audio: false,
image: false,
text: false,
});
const [assignmentType, setAssignmentType] = useState("mediahub");
const [assignmentCategory, setAssignmentCategory] = useState("publication");
const [mainType, setMainType] = useState<number>(1); // untuk Tipe Penugasan
const [type, setType] = useState<string>("1");
const [options, setOptions] = useState<Option[]>([]);
const [ticketPriority, setTicketPriority] = useState([]);
const [selectedOption, setSelectedOption] = useState<Option | undefined>(
undefined
);
const animatedComponent = makeAnimated();
const [platformTypeVisible, setPlatformTypeVisible] = useState(false);
const [selectedOption, setSelectedOption] = useState<Option | undefined>();
const [selectedTarget, setSelectedTarget] = useState<number | null>(null);
const animatedComponent = makeAnimated();
const [isSubmitting, setIsSubmitting] = useState(false); // ⭐ NEW
const priority = [
{ value: 1, label: "Low" },
@ -93,54 +65,27 @@ export default function FormInternal() {
resolver: zodResolver(taskSchema),
});
const handleRadioChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const selectedValue = Number(event.target.value);
setMainType(selectedValue);
setPlatformTypeVisible(selectedValue === 2);
};
useEffect(() => {
getUser();
getTicketPriority();
});
}, []);
const handleChange = (e: any) => {
const selected = e;
setSelectedOption(selected);
setSelectedOption(e);
};
const formatOptionLabel = (option: Option) => (
<>
<div className="row">
<div className="col">
{option.value} | {option.fullname}
</div>
</div>
<div className="row">
<div className="col">
<b>{option.userLevel}</b>
</div>
</div>
</>
);
async function getTicketPriority() {
const res = await getTicketingPriority();
if (res?.data !== null) {
const rawData = res?.data?.data;
setTicketPriority(rawData);
// optional: setTicketPriority(rawData);
}
}
async function getUser() {
const res = await getCuratorUser();
if (res?.data !== null) {
const rawUser = res?.data?.data?.content;
console.log("raw user", rawUser);
const optionArr: Option[] = rawUser.map((option: any) => ({
id: option?.id,
label: option?.username + option?.fullname + option?.userLevel?.name,
@ -149,12 +94,23 @@ export default function FormInternal() {
userLevel: option?.userLevel?.name,
userLevelId: option?.userLevel?.id,
}));
setOptions(optionArr);
}
}
const save = async (data: TaskSchema) => {
setIsSubmitting(true); // ⭐ NEW
MySwal.fire({
title: "Menyimpan...",
text: "Mohon tunggu, data sedang diproses.",
allowOutsideClick: false,
allowEscapeKey: false,
didOpen: () => {
Swal.showLoading();
},
});
try {
const requestData = {
title: data.title,
message: data.message,
@ -167,6 +123,8 @@ export default function FormInternal() {
console.log("Form Data Submitted:", requestData);
console.log("response", response);
Swal.close(); // ⭐ Tutup loading
MySwal.fire({
title: "Sukses",
text: "Data berhasil disimpan.",
@ -176,6 +134,17 @@ export default function FormInternal() {
}).then(() => {
router.push("/in/shared/communication");
});
} catch (error) {
Swal.close(); // Tutup loading jika error
MySwal.fire({
title: "Error",
text: "Terjadi kesalahan saat menyimpan data.",
icon: "error",
});
console.error("Error saat menyimpan data:", error);
} finally {
setIsSubmitting(false); // Reset state
}
};
const onSubmit = (data: TaskSchema) => {
@ -196,11 +165,9 @@ export default function FormInternal() {
return (
<Card>
<div className=" px-6 py-6">
<p className="text-lg font-semibold mb-3"></p>
<div className="px-6 py-6">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex flex-col justify-center items-center gap-5 mb-5">
{/* Input Title */}
<div className="w-6/12 space-y-2">
<Label>
Judul<span className="text-red-500">*</span>
@ -222,8 +189,8 @@ export default function FormInternal() {
<p className="text-red-400 text-sm">{errors.title.message}</p>
)}
</div>
<div className="w-6/12">
<div className="mt-5">
<Label>
Priority <span className="text-red-500">*</span>
</Label>
@ -234,17 +201,11 @@ export default function FormInternal() {
setSelectedTarget(selectedOption?.value ?? null)
}
placeholder="Pilih"
styles={{
control: (base) => ({
...base,
minHeight: "40px",
}),
}}
styles={{ control: (base) => ({ ...base, minHeight: "40px" }) }}
/>
</div>
</div>
<div className="w-6/12">
<div className="mt-5">
<Label>Ditunjukan Untuk</Label>
<Select
options={options}
@ -252,11 +213,10 @@ export default function FormInternal() {
closeMenuOnSelect={false}
components={animatedComponent}
onChange={handleChange}
formatOptionLabel={formatOptionLabel}
isMulti={false}
/>
</div>
</div>
<div className="w-6/12 mt-5">
<Label>Narasi Penugasan</Label>
<Controller
@ -270,12 +230,13 @@ export default function FormInternal() {
<p className="text-red-400 text-sm">{errors.message.message}</p>
)}
</div>
<div className="w-6/12 flex justify-end gap-3 mt-5">
<Button type="button" color="primary" variant="outline">
Batal
</Button>
<Button type="submit" color="primary">
Submit
<Button type="submit" color="primary" disabled={isSubmitting}>
{isSubmitting ? "Menyimpan..." : "Submit"}
</Button>
</div>
</div>

View File

@ -86,35 +86,29 @@ export default function FormAudio() {
type AudioSchema = z.infer<typeof audioSchema>;
const params = useParams();
const locale = params?.locale;
const [selectedFileType, setSelectedFileType] = useState("original");
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const taskId = Cookies.get("taskId");
const scheduleId = Cookies.get("scheduleId");
const scheduleType = Cookies.get("scheduleType");
const [tags, setTags] = useState<string[]>([]);
const roleId = getCookiesDecrypt("urie");
const t = useTranslations("Form");
const [categories, setCategories] = useState<Category[]>([]);
const [selectedCategory, setSelectedCategory] = useState<any>();
const [tags, setTags] = useState<any[]>([]);
const [thumbnail, setThumbnail] = useState<File | null>(null);
const [preview, setPreview] = useState<string | null>(null);
const [selectedLanguage, setSelectedLanguage] = useState("");
const [selectedWritingStyle, setSelectedWritingStyle] =
useState("professional");
const [editorContent, setEditorContent] = useState(""); // Untuk original editor
const [editorContent, setEditorContent] = useState("");
const [rewriteEditorContent, setRewriteEditorContent] = useState("");
const [selectedSEO, setSelectedSEO] = useState<string>("");
const [title, setTitle] = useState<string>("");
const [selectedAdvConfig, setSelectedAdvConfig] = useState<string>("");
const [editingArticleId, setEditingArticleId] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isLoadingData, setIsLoadingData] = useState<boolean>(false);
const [articleIds, setArticleIds] = useState<string[]>([]);
const [isGeneratedArticle, setIsGeneratedArticle] = useState(false);
const [articleBody, setArticleBody] = useState<string>("");
@ -142,11 +136,12 @@ export default function FormAudio() {
let uploadPersen = 0;
const [isStartUpload, setIsStartUpload] = useState(false);
const [counterProgress, setCounterProgress] = useState(0);
const [isContentRewriteClicked, setIsContentRewriteClicked] = useState(false);
const [showRewriteEditor, setShowRewriteEditor] = useState(false);
const [files, setFiles] = useState<FileWithPreview[]>([]);
const [publishedFor, setPublishedFor] = useState<string[]>([]);
const [fileError, setFileError] = useState<string | null>(null);
type FileWithPreview = File & { preview: string };
const options: Option[] = [
{ id: "all", label: "SEMUA" },
@ -159,13 +154,34 @@ export default function FormAudio() {
const audioRefs = useRef<HTMLAudioElement[]>([]);
const { getRootProps, getInputProps } = useDropzone({
accept: {
"audio/mpeg": [".mp3"],
"audio/wav": [".wav"],
},
maxSize: 100 * 1024 * 1024,
multiple: true,
accept: { "audio/*": [] },
onDrop: (acceptedFiles) => {
onDrop: (acceptedFiles, fileRejections) => {
setFileError(null);
if (fileRejections.length > 0) {
const messages = fileRejections
.map((rej) => rej.errors.map((e) => e.message).join(", "))
.join(", ");
setFileError(messages || "File tidak valid");
return;
}
if (acceptedFiles.length === 0) {
setFileError("Wajib upload minimal 1 file mp3 atau wav");
return;
}
const filesWithPreview = acceptedFiles.map((file) =>
Object.assign(file, { preview: URL.createObjectURL(file) })
);
setFiles((prev) => [...prev, ...filesWithPreview]);
setFiles(filesWithPreview);
setValue("files", filesWithPreview, { shouldValidate: true });
},
});
@ -184,8 +200,13 @@ export default function FormAudio() {
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: "Category diperlukan" }),
files: z
.array(z.any())
.min(1, { message: "Wajib upload minimal 1 file mp3 atau wav" }),
tags: z
.array(z.string().min(1))
.min(1, { message: "Wajib isi minimal 1 tag" }),
});
const {
@ -200,6 +221,8 @@ export default function FormAudio() {
description: "",
descriptionOri: "",
rewriteDescription: "",
category: "",
tags: [],
},
});
@ -398,20 +421,18 @@ export default function FormAudio() {
const handleAddTag = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && e.currentTarget.value.trim()) {
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 newTags = [...tags, e.currentTarget.value.trim()];
setTags(newTags);
setValue("tags", newTags, { shouldValidate: true });
e.currentTarget.value = "";
}
};
const handleRemoveTag = (index: number) => {
setTags((prevTags) => prevTags.filter((_, i) => i !== index)); // Remove tag
const newTags = tags.filter((_, i) => i !== index);
setTags(newTags);
setValue("tags", newTags, { shouldValidate: true });
};
const handleRemoveImage = (index: number) => {
setSelectedFiles((prevImages) => prevImages.filter((_, i) => i !== index));
};
@ -441,7 +462,7 @@ export default function FormAudio() {
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);
}
@ -514,7 +535,7 @@ export default function FormAudio() {
tags: string;
isYoutube: boolean;
isInternationalMedia: boolean;
attachFromScheduleId?: number; // ✅ Tambahkan properti ini
attachFromScheduleId?: number;
} = {
...data,
title: finalTitle,
@ -535,7 +556,7 @@ export default function FormAudio() {
let id = Cookies.get("idCreate");
if (scheduleId !== undefined) {
requestData.attachFromScheduleId = Number(scheduleId); // ✅ Tambahkan nilai ini
requestData.attachFromScheduleId = Number(scheduleId);
}
if (id == undefined) {
@ -844,9 +865,16 @@ export default function FormAudio() {
<div className="flex items-center">
<div className="py-3 w-full space-y-2">
<Label>{t("category", { defaultValue: "Category" })}</Label>
<Controller
name="category"
control={control}
rules={{ required: "Category is required" }}
render={({ field, fieldState }) => (
<>
<Select
value={selectedCategory}
value={field.value}
onValueChange={(id) => {
field.onChange(id);
console.log("Selected Category ID:", id);
setSelectedCategory(id);
}}
@ -865,6 +893,14 @@ export default function FormAudio() {
))}
</SelectContent>
</Select>
{fieldState.error && (
<p className="text-sm text-red-500">
{fieldState.error.message}
</p>
)}
</>
)}
/>
</div>
</div>
<div className="flex flex-row items-center gap-3 py-2">
@ -1234,7 +1270,14 @@ export default function FormAudio() {
<Label>
{t("select-file", { defaultValue: "Select File" })}
</Label>
<Fragment>
<Controller
name="files"
control={control}
rules={{
required: "Wajib upload minimal 1 file mp3 atau wav",
}}
render={({ field, fieldState }) => (
<>
<div {...getRootProps({ className: "dropzone" })}>
<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">
@ -1250,8 +1293,19 @@ export default function FormAudio() {
</div>
</div>
{/* pesan error dari validasi dropzone */}
{fileError && (
<p className="text-sm text-red-500 mt-1">{fileError}</p>
)}
{/* pesan error dari schema */}
{fieldState.error && (
<p className="text-sm text-red-500 mt-1">
{fieldState.error.message}
</p>
)}
{files.length ? (
<Fragment>
<div className="space-y-4 mt-4">
{files.map((file, idx) => (
<div
@ -1263,10 +1317,7 @@ export default function FormAudio() {
</p>
<audio
controls
src={file.preview ?? URL.createObjectURL(file)}
ref={(el) => {
if (el) audioRefs.current[idx] = el;
}}
src={file.preview}
onPlay={() => handlePlay(idx)}
className="w-full rounded"
>
@ -1275,17 +1326,25 @@ export default function FormAudio() {
</div>
))}
</div>
) : null}
{files.length > 0 && (
<div className="flex justify-between gap-2 mt-3">
<Button
color="destructive"
onClick={handleRemoveAllFiles}
onClick={() => {
setFiles([]);
setFileError(null);
setValue("files", [], { shouldValidate: true });
}}
>
{t("remove-all", { defaultValue: "Remove All" })}
</Button>
</div>
</Fragment>
) : null}
</Fragment>
)}
</>
)}
/>
</div>
</div>
</div>
@ -1327,11 +1386,12 @@ export default function FormAudio() {
onKeyDown={handleAddTag}
ref={inputRef}
/>
<div className="mt-3 ">
<div className="mt-3">
{tags.map((tag, index) => (
<span
key={index}
className=" px-1 py-1 rounded-lg bg-black text-white mr-2 text-sm font-sans"
className="px-1 py-1 rounded-lg bg-black text-white mr-2 text-sm font-sans"
>
{tag}{" "}
<button
@ -1344,7 +1404,14 @@ export default function FormAudio() {
</span>
))}
</div>
{errors.tags && (
<p className="text-sm text-red-500 mt-1">
{errors.tags.message}
</p>
)}
</div>
<div className="px-3 py-3">
<div className="flex flex-col gap-3 space-y-2">
<Label>

View File

@ -237,7 +237,7 @@ export default function FormVideoDetail() {
format: details?.files[0]?.format,
});
if (details.publishedForObject) {
if (details?.publishedForObject) {
const publisherIds = details.publishedForObject.map(
(obj: any) => obj.id
);
@ -453,7 +453,7 @@ export default function FormVideoDetail() {
<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);

View File

@ -92,7 +92,6 @@ export default function FormVideo() {
const scheduleType = Cookies.get("scheduleType");
const roleId = getCookiesDecrypt("urie");
const [selectedFileType, setSelectedFileType] = useState("original");
const t = useTranslations("Form");
const [categories, setCategories] = useState<Category[]>([]);
const [selectedCategory, setSelectedCategory] = useState<any>();
@ -100,19 +99,16 @@ export default function FormVideo() {
const [thumbnail, setThumbnail] = useState<File | null>(null);
const [preview, setPreview] = useState<string | null>(null);
const [selectedLanguage, setSelectedLanguage] = useState("");
const [selectedWritingStyle, setSelectedWritingStyle] =
useState("professional");
const [editorContent, setEditorContent] = useState(""); // Untuk original editor
const [editorContent, setEditorContent] = useState("");
const [rewriteEditorContent, setRewriteEditorContent] = useState("");
const [selectedSEO, setSelectedSEO] = useState<string>("");
const [title, setTitle] = useState<string>("");
const [selectedAdvConfig, setSelectedAdvConfig] = useState<string>("");
const [editingArticleId, setEditingArticleId] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isLoadingData, setIsLoadingData] = useState<boolean>(false);
const [articleIds, setArticleIds] = useState<string[]>([]);
const [isGeneratedArticle, setIsGeneratedArticle] = useState(false);
const [articleBody, setArticleBody] = useState<string>("");
@ -126,11 +122,9 @@ export default function FormVideo() {
const [articleImages, setArticleImages] = useState<string[]>([]);
const [isSwitchOn, setIsSwitchOn] = useState<boolean>(false);
const inputRef = useRef<HTMLInputElement>(null);
const [fileError, setFileError] = useState<string | null>(null);
const [showRewriteEditor, setShowRewriteEditor] = useState(false);
const [isContentRewriteClicked, setIsContentRewriteClicked] = useState(false);
const [selectedTarget, setSelectedTarget] = useState("");
const [unitSelection, setUnitSelection] = useState({
allUnit: false,
@ -165,21 +159,33 @@ export default function FormVideo() {
"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
maxSize: 500 * 1024 * 1024,
multiple: true,
onDrop: (acceptedFiles, fileRejections) => {
setFileError(null);
if (fileRejections.length > 0) {
const messages = fileRejections
.map((rej) => rej.errors.map((e) => e.message).join(", "))
.join(", ");
setFileError(messages || "File tidak valid");
return;
}
if (acceptedFiles.length === 0) {
setFileError("Wajib upload minimal 1 file video");
return;
}
const filesWithPreview = acceptedFiles.map((file) =>
Object.assign(file, { preview: URL.createObjectURL(file) })
);
const filesWithPreview = filteredFiles.map((file) =>
Object.assign(file, {
preview: URL.createObjectURL(file),
})
);
setFiles(filesWithPreview);
setValue("files", filesWithPreview);
setFiles((prev) => {
const updatedFiles = [...prev, ...filesWithPreview];
setValue("files", updatedFiles, { shouldValidate: true });
return updatedFiles;
});
},
});

View File

@ -668,9 +668,27 @@ export default function FormContestDetail() {
<Checkbox
id={key}
checked={taskOutput[key as keyof typeof taskOutput]}
onCheckedChange={(value) =>
setTaskOutput({ ...taskOutput, [key]: value })
onCheckedChange={(value) => {
if (key === "all") {
const newValue = Boolean(value);
setTaskOutput({
all: newValue,
video: newValue,
audio: newValue,
image: newValue,
text: newValue,
});
} else {
setTaskOutput((prev) => {
const updated = { ...prev, [key]: Boolean(value) };
// Update 'all' jika semua sub-checkbox true
const allChecked = Object.entries(updated)
.filter(([k]) => k !== "all")
.every(([_, v]) => v);
return { ...updated, all: allChecked };
});
}
}}
/>
<Label htmlFor={key}>
{key.charAt(0).toUpperCase() + key.slice(1)}
@ -687,9 +705,27 @@ export default function FormContestDetail() {
<Checkbox
id={key}
checked={unitSelection[key as keyof typeof unitSelection]}
onCheckedChange={(value) =>
setUnitSelection({ ...unitSelection, [key]: value })
onCheckedChange={(value) => {
if (key === "allUnit") {
const newValue = Boolean(value);
setUnitSelection({
allUnit: newValue,
mabes: newValue,
polda: newValue,
polres: newValue,
satker: newValue,
});
} else {
setUnitSelection((prev) => {
const updated = { ...prev, [key]: Boolean(value) };
// Update 'allUnit' jika semua sub-checkbox true
const allChecked = Object.entries(updated)
.filter(([k]) => k !== "allUnit")
.every(([_, v]) => v);
return { ...updated, allUnit: allChecked };
});
}
}}
/>
<Label htmlFor={key}>
{key.charAt(0).toUpperCase() + key.slice(1)}

View File

@ -41,6 +41,7 @@ import {
} from "@/components/ui/select";
import { toast } from "sonner";
import { close, loading } from "@/config/swal";
import { useTranslations } from "next-intl";
const taskSchema = z.object({
title: z.string().min(1, { message: "Judul harus diisi" }),
@ -59,6 +60,7 @@ export default function FormLiveReport() {
const [endTime, setEndTime] = useState("09:00");
const [scheduleTypeId, setScheduleTypeId] = React.useState<string>("1");
const [youtubeUrl, setYoutubeUrl] = useState("");
const t = useTranslations("Form");
const [date, setDate] = React.useState<DateRange | undefined>({
from: new Date(),
@ -233,7 +235,7 @@ export default function FormLiveReport() {
</div>
<div className="flex flex-col lg:flex-row mb-4 mt-2 items-start lg:items-center justify-between">
<div className="flex flex-col space-y-2">
<Label className="mr-3 mb-1">Tanggal</Label>
<Label className="mr-3 mb-1">{t("date")}</Label>
<Popover>
<PopoverTrigger asChild className="px-0">
<Button

View File

@ -254,6 +254,9 @@ export default function FormTask() {
};
const save = async (data: TaskSchema) => {
try {
loading();
const fileTypeMapping = {
all: "1",
video: "2",
@ -261,7 +264,6 @@ export default function FormTask() {
image: "3",
text: "5",
};
const unitMapping = {
allUnit: "0",
mabes: "1",
@ -273,35 +275,18 @@ export default function FormTask() {
.filter((key) => unitSelection[key as keyof typeof unitSelection])
.map((key) => unitMapping[key as keyof typeof unitMapping])
.join(",");
const selectedOutputs = Object.keys(taskOutput)
.filter((key) => taskOutput[key as keyof typeof taskOutput])
.map((key) => fileTypeMapping[key as keyof typeof fileTypeMapping])
.join(",");
const requestData: {
id?: number;
title: string;
assignedToLevel: any;
assignmentPurpose: any;
assignmentTypeId: string;
fileTypeOutput: string;
narration: string;
platformType: string | null;
assignmentMainTypeId: any;
taskType: string;
assignedToRole: string;
broadcastType: string;
attachmentUrl: string[];
} = {
const requestData = {
...data,
// assignmentType,
// assignmentCategory,
assignedToLevel: handlePoldaPolresChange(),
assignmentPurpose: assignmentPurposeString,
assignedToRole: selectedTarget,
taskType: taskType,
broadcastType: broadcastType,
taskType,
broadcastType,
assignmentMainTypeId: mainType,
assignmentTypeId: type,
fileTypeOutput: selectedOutputs,
@ -312,50 +297,41 @@ export default function FormTask() {
};
const response = await createTask(requestData);
console.log("Form Data Submitted:", requestData);
console.log("response", response);
const id = response?.data?.data.id;
loading();
if (imageFiles?.length == 0) {
setIsImageUploadFinish(true);
} else {
imageFiles?.map(async (item: any, index: number) => {
await uploadResumableFile(index, String(id), item, "1", "0");
});
}
if (videoFiles?.length == 0) {
setIsVideoUploadFinish(true);
} else {
videoFiles?.map(async (item: any, index: number) => {
await uploadResumableFile(index, String(id), item, "2", "0");
});
}
if (textFiles?.length == 0) {
setIsTextUploadFinish(true);
} else {
textFiles?.map(async (item: any, index: number) => {
await uploadResumableFile(index, String(id), item, "3", "0");
});
}
if (audioFiles?.length == 0) {
setIsAudioUploadFinish(true);
} else {
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
if (imageFiles?.length === 0) setIsImageUploadFinish(true);
else
imageFiles.map(
async (item, idx) =>
await uploadResumableFile(idx, String(id), item, "1", "0")
);
});
}
if (videoFiles?.length === 0) setIsVideoUploadFinish(true);
else
videoFiles.map(
async (item, idx) =>
await uploadResumableFile(idx, String(id), item, "2", "0")
);
if (textFiles?.length === 0) setIsTextUploadFinish(true);
else
textFiles.map(
async (item, idx) =>
await uploadResumableFile(idx, String(id), item, "3", "0")
);
if (audioFiles?.length === 0) setIsAudioUploadFinish(true);
else
audioFiles.map(
async (item, idx) =>
await uploadResumableFile(idx, String(id), item, "4", "0")
);
} catch (err) {
console.error("Error saat kirim data:", err);
close();
error("Terjadi kesalahan saat mengirim data.");
}
};
const onSubmit = (data: TaskSchema) => {
@ -403,7 +379,7 @@ export default function FormTask() {
const handleStopRecording = () => {
setIsRecording(false);
setTimer(120); // Reset the timer to 2 minutes for the next recording
setTimer(120);
};
const addAudioElement = (blob: Blob) => {
@ -413,13 +389,11 @@ export default function FormTask() {
audio.controls = true;
document.body.appendChild(audio);
// 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]);
};
@ -542,7 +516,6 @@ export default function FormTask() {
setLinks([...links, ""]);
};
// Remove a specific link row
const handleRemoveRow = (index: number) => {
const updatedLinks = links.filter((_: any, i: any) => i !== index);
setLinks(updatedLinks);
@ -551,7 +524,9 @@ export default function FormTask() {
return (
<Card>
<div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">{t("form-task", { defaultValue: "Form Task" })}</p>
<p className="text-lg font-semibold mb-3">
{t("form-task", { defaultValue: "Form Task" })}
</p>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="gap-5 mb-5">
@ -577,7 +552,11 @@ export default function FormTask() {
</div>
<div className="flex flex-col sm:flex-row lg:flex-row sm:items-center lg:items-center">
<div className="mt-5 space-y-2">
<Label>{t("assignment-selection", { defaultValue: "Assignment Selection" })}</Label>
<Label>
{t("assignment-selection", {
defaultValue: "Assignment Selection",
})}
</Label>
<Select onValueChange={setSelectedTarget}>
<SelectTrigger size="md">
<SelectValue placeholder="Choose" />
@ -706,7 +685,9 @@ export default function FormTask() {
</RadioGroup>
</div>
<div className="mt-5 space-y-2">
<Label>{t("assigment-type", { defaultValue: "Assigment Type" })} </Label>
<Label>
{t("assigment-type", { defaultValue: "Assigment Type" })}{" "}
</Label>
<RadioGroup
value={taskType}
onValueChange={(value) => setTaskType(String(value))}
@ -720,7 +701,9 @@ export default function FormTask() {
</div>
{/* RadioGroup Assignment Category */}
<div className="mt-5 space-y-2">
<Label>{t("type-of-task", { defaultValue: "Type Of Task" })}</Label>
<Label>
{t("type-of-task", { defaultValue: "Type Of Task" })}
</Label>
<RadioGroup
value={type} // State yang dipetakan ke value RadioGroup
onValueChange={(value) => setType(value)} // Mengubah nilai state ketika pilihan berubah
@ -799,10 +782,14 @@ export default function FormTask() {
)}
</div>
<div className="space-y-2.5 mt-5">
<Label htmlFor="attachments">{t("attachment", { defaultValue: "Attachment" })}</Label>
<Label htmlFor="attachments">
{t("attachment", { defaultValue: "Attachment" })}
</Label>
<div className="space-y-3">
<div>
<Label>{t("audio-visual", { defaultValue: "Audio Visual" })}</Label>
<Label>
{t("audio-visual", { defaultValue: "Audio Visual" })}
</Label>
<FileUploader
accept={{
"video/*": [],
@ -876,7 +863,9 @@ export default function FormTask() {
{isRecording && <p>Recording... {timer} seconds remaining</p>}{" "}
{/* Display remaining time */}
<div className="mt-4 space-y-2">
<Label className="">{t("news-links", { defaultValue: "News Links" })}</Label>
<Label className="">
{t("news-links", { defaultValue: "News Links" })}
</Label>
{links.map((link, index) => (
<div key={index} className="flex items-center gap-2 mt-2">
<Input

View File

@ -1,4 +1,3 @@
import search from "@/app/[locale]/(protected)/app/chat/components/search";
import { useTranslations } from "next-intl";
import { useParams } from "next/navigation";
import router from "next/router";

View File

@ -1,4 +1,3 @@
import search from "@/app/[locale]/(protected)/app/chat/components/search";
import {
Select,
SelectTrigger,

View File

@ -190,7 +190,7 @@ const DetailAudio = () => {
const handleDownload = () => {
if (downloadProgress === 0) {
if (!userId) {
router.push("/auth/login");
router.push("/auth");
} else {
sendActivityLog(2);
sendActivityLog(3);

View File

@ -185,7 +185,7 @@ const DetailDocument = () => {
const handleDownload = () => {
if (downloadProgress === 0) {
if (!userId) {
router.push("/auth/login");
router.push("/auth");
} else {
sendActivityLog(2);
sendActivityLog(3);

View File

@ -222,7 +222,7 @@ const DetailImage = (data: any) => {
const handleDownload = () => {
if (downloadProgress === 0) {
if (!userId) {
router.push("/auth/login");
router.push("/auth");
} else {
sendActivityLog(2);
sendActivityLog(3);
@ -676,7 +676,7 @@ const DetailImage = (data: any) => {
key={size?.label}
className="items-center flex flex-row gap-2 cursor-pointer"
>
<input
<Input
type="radio"
name="size"
value={size?.label}

View File

@ -182,7 +182,7 @@ const DetailVideo = () => {
const handleDownload = () => {
if (downloadProgress === 0) {
if (!userId) {
router.push("/auth/login");
router.push("/auth");
} else {
sendActivityLog(2);
sendActivityLog(3);
@ -404,7 +404,6 @@ const DetailVideo = () => {
console.log(responseGet?.data?.data);
setListSuggestion(responseGet?.data?.data);
// Hapus nilai semua input secara manual jika perlu
const inputs = document.querySelectorAll("input");
inputs.forEach((input) => {
input.value = "";
@ -465,7 +464,6 @@ const DetailVideo = () => {
return (
<>
<div className="px-4 md:px-24 py-4">
{/* Container Utama */}
<div className="rounded-md overflow-hidden md:flex">
{/* Bagian Kiri */}
<div className="md:w-3/4">

View File

@ -1,11 +1,10 @@
import React, { Component } from "react";
import Places from "./Maps"; // Pastikan ini adalah komponen Places
import Places from "./Maps";
interface MapHomeProps {
newLat?: any;
newLng?: any;
draggable?: boolean;
setLocation: (location: string) => void; // Pastikan properti ini ada
setLocation: (location: string) => void;
}
class MapHome extends Component<MapHomeProps> {
@ -20,7 +19,7 @@ class MapHome extends Component<MapHomeProps> {
<Places
center={{ lat: Number(lat), lng: Number(lng) }}
draggable={draggable}
onLocationChange={setLocation} // Kirimkan setLocation ke Places
onLocationChange={setLocation}
/>
</div>
);

View File

@ -14,7 +14,7 @@ Geocode.setApiKey(GoogleMapsAPI);
export default function Places(props: {
center: { lat: number; lng: number };
draggable?: boolean;
onLocationChange?: (location: string) => void; // Tambahkan onLocationChange
onLocationChange?: (location: string) => void;
}) {
const { isLoaded } = useLoadScript({
googleMapsApiKey: GoogleMapsAPI,
@ -31,7 +31,7 @@ export default function Places(props: {
lat={center.lat}
lng={center.lng}
draggable={draggable}
onLocationChange={onLocationChange} // Kirimkan properti onLocationChange
onLocationChange={onLocationChange}
/>
);
}
@ -40,7 +40,7 @@ interface MapProps {
lat: number;
lng: number;
draggable?: boolean;
onLocationChange?: (location: string) => void; // Tambahkan properti ini
onLocationChange?: (location: string) => void;
}
function Map(props: MapProps) {
@ -73,7 +73,7 @@ function Map(props: MapProps) {
console.log(lat, lng);
getAddressFromLatLong(lat, lng);
if (onLocationChange) {
onLocationChange(`Latitude: ${lat}, Longitude: ${lng}`); // Kirimkan lokasi ke parent melalui onLocationChange
onLocationChange(`Latitude: ${lat}, Longitude: ${lng}`);
}
};
@ -85,7 +85,7 @@ function Map(props: MapProps) {
Cookies.set("map_long", `${lng}`, { expires: 1 });
console.log("Address:", address);
if (onLocationChange) {
onLocationChange(address); // Kirimkan alamat jika berhasil
onLocationChange(address);
}
} catch (error) {
console.error(error);

View File

@ -1 +1 @@
export const GoogleMapsAPI = "AIzaSyCOkxoeKykE60L_nM4VS1JYJqBmqy2GA0Q";
export const GoogleMapsAPI = "AIzaSyCuQHorDceMCzlSgrB9AEY5ns8KeriFsME";

345
package-lock.json generated
View File

@ -173,6 +173,7 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
"dev": true,
"engines": {
"node": ">=10"
},
@ -1987,6 +1988,7 @@
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dev": true,
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
@ -2003,6 +2005,7 @@
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"dev": true,
"engines": {
"node": ">=12"
},
@ -2014,6 +2017,7 @@
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"dependencies": {
"ansi-regex": "^6.0.1"
},
@ -2633,166 +2637,6 @@
"node": ">=18"
}
},
"node_modules/@mui/core-downloads-tracker": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.2.0.tgz",
"integrity": "sha512-d49s7kEgI5iX40xb2YPazANvo7Bx0BLg/MNRwv+7BVpZUzXj1DaVCKlQTDex3gy/0jsCb4w7AY2uH4t4AJvSog==",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
}
},
"node_modules/@mui/material": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.2.0.tgz",
"integrity": "sha512-NTuyFNen5Z2QY+I242MDZzXnFIVIR6ERxo7vntFi9K1wCgSwvIl0HcAO2OOydKqqKApE6omRiYhpny1ZhGuH7Q==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.27.6",
"@mui/core-downloads-tracker": "^7.2.0",
"@mui/system": "^7.2.0",
"@mui/types": "^7.4.4",
"@mui/utils": "^7.2.0",
"@popperjs/core": "^2.11.8",
"@types/react-transition-group": "^4.4.12",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1",
"react-is": "^19.1.0",
"react-transition-group": "^4.4.5"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@mui/material-pigment-css": "^7.2.0",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@mui/material-pigment-css": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/private-theming": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.2.0.tgz",
"integrity": "sha512-y6N1Yt3T5RMxVFnCh6+zeSWBuQdNDm5/UlM0EAYZzZR/1u+XKJWYQmbpx4e+F+1EpkYi3Nk8KhPiQDi83M3zIw==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.27.6",
"@mui/utils": "^7.2.0",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/styled-engine": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.2.0.tgz",
"integrity": "sha512-yq08xynbrNYcB1nBcW9Fn8/h/iniM3ewRguGJXPIAbHvxEF7Pz95kbEEOAAhwzxMX4okhzvHmk0DFuC5ayvgIQ==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.27.6",
"@emotion/cache": "^11.14.0",
"@emotion/serialize": "^1.3.3",
"@emotion/sheet": "^1.4.0",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
}
}
},
"node_modules/@mui/system": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-7.2.0.tgz",
"integrity": "sha512-PG7cm/WluU6RAs+gNND2R9vDwNh+ERWxPkqTaiXQJGIFAyJ+VxhyKfzpdZNk0z0XdmBxxi9KhFOpgxjehf/O0A==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.27.6",
"@mui/private-theming": "^7.2.0",
"@mui/styled-engine": "^7.2.0",
"@mui/types": "^7.4.4",
"@mui/utils": "^7.2.0",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/types": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.4.tgz",
@ -3381,6 +3225,7 @@
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dev": true,
"optional": true,
"engines": {
"node": ">=14"
@ -5017,78 +4862,6 @@
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@testing-library/dom": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
"integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==",
"dev": true,
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
"@types/aria-query": "^5.0.1",
"aria-query": "5.3.0",
"chalk": "^4.1.0",
"dom-accessibility-api": "^0.5.9",
"lz-string": "^1.5.0",
"pretty-format": "^27.0.2"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@testing-library/dom/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"peer": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@testing-library/dom/node_modules/aria-query": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"dev": true,
"peer": true,
"dependencies": {
"dequal": "^2.0.3"
}
},
"node_modules/@testing-library/dom/node_modules/dom-accessibility-api": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"peer": true
},
"node_modules/@testing-library/dom/node_modules/pretty-format": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
"react-is": "^17.0.1"
},
"engines": {
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
}
},
"node_modules/@testing-library/dom/node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"peer": true
},
"node_modules/@testing-library/jest-dom": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz",
@ -5266,13 +5039,6 @@
"@types/estree": "*"
}
},
"node_modules/@types/aria-query": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"peer": true
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@ -6312,12 +6078,14 @@
"node_modules/any-promise": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
"dev": true
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
@ -6330,6 +6098,7 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
@ -6373,7 +6142,8 @@
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"dev": true
},
"node_modules/argparse": {
"version": "2.0.1",
@ -6824,6 +6594,7 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"engines": {
"node": ">=8"
},
@ -7045,6 +6816,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
"dev": true,
"engines": {
"node": ">= 6"
}
@ -7172,6 +6944,7 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
@ -7195,6 +6968,7 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"dependencies": {
"is-glob": "^4.0.1"
},
@ -7716,6 +7490,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true,
"bin": {
"cssesc": "bin/cssesc"
},
@ -8507,7 +8282,8 @@
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
"dev": true
},
"node_modules/diff": {
"version": "5.2.0",
@ -8532,7 +8308,8 @@
"node_modules/dlv": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"dev": true
},
"node_modules/doctrine": {
"version": "3.0.0",
@ -8681,7 +8458,8 @@
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true
},
"node_modules/ee-first": {
"version": "1.1.1",
@ -8743,16 +8521,11 @@
"url": "https://github.com/sindresorhus/emittery?sponsor=1"
}
},
"node_modules/emoji-mart": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-5.6.0.tgz",
"integrity": "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==",
"peer": true
},
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true
},
"node_modules/encodeurl": {
"version": "2.0.0",
@ -9980,6 +9753,7 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"dev": true,
"dependencies": {
"cross-spawn": "^7.0.6",
"signal-exit": "^4.0.1"
@ -10076,6 +9850,7 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
@ -10286,6 +10061,7 @@
"version": "10.3.10",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
"integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
"dev": true,
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^2.3.5",
@ -10307,6 +10083,7 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"dependencies": {
"is-glob": "^4.0.3"
},
@ -10318,6 +10095,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0"
}
@ -10326,6 +10104,7 @@
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
@ -11512,6 +11291,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"dependencies": {
"binary-extensions": "^2.0.0"
},
@ -12111,6 +11891,7 @@
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
"integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
"dev": true,
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
@ -12785,6 +12566,7 @@
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true,
"bin": {
"jiti": "bin/jiti.js"
}
@ -13331,7 +13113,8 @@
"node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true
},
"node_modules/lucide-react": {
"version": "0.390.0",
@ -13341,16 +13124,6 @@
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/lz-string": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
},
"node_modules/make-dir": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
@ -14982,6 +14755,7 @@
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"engines": {
"node": ">=16 || 14 >=14.17"
}
@ -15095,6 +14869,7 @@
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
"dev": true,
"dependencies": {
"any-promise": "^1.0.0",
"object-assign": "^4.0.1",
@ -15435,6 +15210,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@ -15493,6 +15269,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"dev": true,
"engines": {
"node": ">= 6"
}
@ -15998,6 +15775,7 @@
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"dev": true,
"dependencies": {
"lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
@ -16060,6 +15838,7 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@ -16068,6 +15847,7 @@
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
"integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
"dev": true,
"engines": {
"node": ">= 6"
}
@ -16184,6 +15964,7 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
"integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
"dev": true,
"dependencies": {
"camelcase-css": "^2.0.1"
},
@ -16202,6 +15983,7 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
"integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
@ -16236,6 +16018,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
"dev": true,
"engines": {
"node": ">=14"
},
@ -16247,6 +16030,7 @@
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
"integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
"dev": true,
"bin": {
"yaml": "bin.mjs"
},
@ -16258,6 +16042,7 @@
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
@ -16282,6 +16067,7 @@
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@ -16293,7 +16079,8 @@
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true
},
"node_modules/preact": {
"version": "10.12.1",
@ -17030,6 +16817,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
"dev": true,
"dependencies": {
"pify": "^2.3.0"
}
@ -17051,6 +16839,7 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"dependencies": {
"picomatch": "^2.2.1"
},
@ -17062,6 +16851,7 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
@ -18440,6 +18230,7 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
@ -18457,6 +18248,7 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@ -18469,12 +18261,14 @@
"node_modules/string-width-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
"node_modules/string-width/node_modules/ansi-regex": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"dev": true,
"engines": {
"node": ">=12"
},
@ -18486,6 +18280,7 @@
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"dependencies": {
"ansi-regex": "^6.0.1"
},
@ -18657,6 +18452,7 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
@ -18783,6 +18579,7 @@
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
"integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
"dev": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.2",
"commander": "^4.0.0",
@ -18804,6 +18601,7 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
"dev": true,
"engines": {
"node": ">= 6"
}
@ -18919,6 +18717,7 @@
"version": "3.4.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
"dev": true,
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@ -18963,6 +18762,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
"dev": true,
"engines": {
"node": ">=14"
},
@ -18974,6 +18774,7 @@
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
"dev": true,
"dependencies": {
"postcss-value-parser": "^4.0.0",
"read-cache": "^1.0.0",
@ -19040,6 +18841,7 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
"dev": true,
"dependencies": {
"any-promise": "^1.0.0"
}
@ -19048,6 +18850,7 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
"dev": true,
"dependencies": {
"thenify": ">= 3.1.0 < 4"
},
@ -19297,7 +19100,8 @@
"node_modules/ts-interface-checker": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"dev": true
},
"node_modules/ts-morph": {
"version": "18.0.0",
@ -19507,7 +19311,7 @@
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"devOptional": true,
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -20396,6 +20200,7 @@
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
@ -20413,6 +20218,7 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
@ -20428,12 +20234,14 @@
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@ -20447,6 +20255,7 @@
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"dev": true,
"engines": {
"node": ">=12"
},
@ -20458,6 +20267,7 @@
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"engines": {
"node": ">=12"
},
@ -20469,6 +20279,7 @@
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"dependencies": {
"ansi-regex": "^6.0.1"
},