This commit is contained in:
hanif salafi 2025-07-22 14:35:47 +07:00
commit 16cbdcff45
27 changed files with 2679 additions and 626 deletions

View File

@ -10,6 +10,7 @@ RUN npm install -g pnpm
# Membuat direktori aplikasi dan mengatur sebagai working directory # Membuat direktori aplikasi dan mengatur sebagai working directory
WORKDIR /usr/src/app WORKDIR /usr/src/app
# Menyalin file penting terlebih dahulu untuk caching # Menyalin file penting terlebih dahulu untuk caching
COPY package.json ./ COPY package.json ./

View File

@ -0,0 +1,743 @@
"use client";
import React, { useEffect, useState } from "react";
import { Checkbox } from "@/components/ui/checkbox";
import { Icon } from "@iconify/react/dist/iconify.js";
import { getOnlyDate, getOnlyMonthAndYear } from "@/utils/globals";
import { useParams, usePathname, useSearchParams } from "next/navigation";
import {
getPublicCategoryData,
getUserLevelListByParent,
listCategory,
listData,
listDataAll,
listDataRegional,
} from "@/service/landing/landing";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
} from "@tanstack/react-table";
import { Link, useRouter } from "@/i18n/routing";
import { Input } from "@/components/ui/input";
import ReactDatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import { close, loading } from "@/config/swal";
import FilterImageComponent from "@/components/landing-page/filter-all/image-filter-card";
import FilterVideoComponent from "@/components/landing-page/filter-all/video-filter-card";
import FilterDocumentComponent from "@/components/landing-page/filter-all/document-filter-card";
import FilterAudioComponent from "@/components/landing-page/filter-all/audio-filter-card";
import { useTranslations } from "next-intl";
export default function FilterPage() {
const router = useRouter();
const asPath = usePathname();
const params = useParams();
const searchParams = useSearchParams();
const locale = params?.locale;
const [imageData, setImageData] = useState<any>();
const [totalData, setTotalData] = React.useState<number>(1);
const [totalPage, setTotalPage] = React.useState<number>(1);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const [page, setPage] = useState(1);
const [totalContent, setTotalContent] = useState();
const [change, setChange] = useState(false);
const sortBy = searchParams?.get("sortBy");
const title = searchParams?.get("title");
const categorie = searchParams?.get("category");
const group = searchParams?.get("group");
const tag: any = searchParams?.get("tag");
const [contentImage, setContentImage] = useState([]);
const [, setGetTotalPage] = useState();
let typingTimer: any;
const doneTypingInterval = 1500;
const [search, setSearch] = useState();
const [categoryFilter, setCategoryFilter] = useState<any>([]);
const [monthYearFilter, setMonthYearFilter] = useState<any>();
const [searchTitle, setSearchTitle] = useState<string>("");
const [sortByOpt, setSortByOpt] = useState<any>(
sortBy === "popular" ? "clickCount" : "createdAt"
);
const isRegional = asPath?.includes("regional");
const isSatker = asPath?.includes("satker");
const [formatFilter, setFormatFilter] = useState<any>([]);
const pages = page ? page - 1 : 0;
const [startDateString, setStartDateString] = useState<any>();
const [endDateString, setEndDateString] = useState<any>();
const [dateRange, setDateRange] = useState<any>([null, null]);
const [calenderState, setCalenderState] = useState(false);
const [handleClose, setHandleClose] = useState(false);
const [categories, setCategories] = useState([]);
const [userLevels, setUserLevels] = useState([]);
const [contentAll, setContentAll] = useState([]);
const t = useTranslations("FilterPage");
const [isFilterOpen, setIsFilterOpen] = useState(true);
const [categoryPage, setCategoryPage] = useState(1);
const [categoryTotalPages, setCategoryTotalPages] = useState(1);
const poldaName = params?.polda_name;
const satkerName = params?.satker_name;
useEffect(() => {
const title = searchParams?.get("title") || "";
const category = searchParams?.get("category") || "";
const sortBy = searchParams?.get("sortBy") || "latest";
setSearchTitle(title);
setCategoryFilter(category ? category.split("&") : []);
setSortByOpt(sortBy === "popular" ? "clickCount" : "createdAt");
}, [searchParams]);
useEffect(() => {
fetchCategories(categoryPage);
}, [categoryPage]);
const fetchCategories = async (pageNumber: number) => {
const groupParam =
poldaName && poldaName.length > 1
? poldaName
: satkerName && satkerName.length > 1
? "satker-" + satkerName
: "";
const isInt = locale === "en";
const response = await getPublicCategoryData(
groupParam,
"",
isInt,
pageNumber
);
const content = response?.data?.data?.content || [];
const total = response?.data?.data?.totalPages || 1;
setCategories(content);
setCategoryTotalPages(total);
};
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
useEffect(() => {
async function initState() {
getCategories();
// getSelectedCategory();
if (isSatker) {
getUserLevels();
}
}
initState();
}, []);
useEffect(() => {
if (categorie) {
setCategoryFilter(
categorie?.split("&")?.length > 1 ? categorie?.split("&") : [categorie]
);
console.log(
"Kategori",
categorie,
categorie?.split("&")?.length > 1 ? categorie?.split("&") : [categorie]
);
}
}, [categorie]);
// useEffect(() => {
// fetchData();
// }, [page, sortBy, sortByOpt, title]);
useEffect(() => {
async function initState() {
if (isRegional) {
getDataRegional();
} else {
getData();
}
}
console.log(monthYearFilter, "monthFilter");
initState();
}, [
change,
monthYearFilter,
sortBy,
sortByOpt,
title,
startDateString,
endDateString,
categorie,
formatFilter,
]);
async function getCategories() {
const category = await listCategory("1");
const resCategory = category?.data?.data?.content;
setCategories(resCategory);
}
useEffect(() => {
function initState() {
if (dateRange[0] != null && dateRange[1] != null) {
setStartDateString(getOnlyDate(dateRange[0]));
setEndDateString(getOnlyDate(dateRange[1]));
setHandleClose(true);
console.log("date range", dateRange, getOnlyDate(dateRange[0]));
}
}
initState();
}, [calenderState]);
async function getData() {
if (asPath?.includes("/polda/") == true) {
if (asPath?.split("/")[2] !== "[polda_name]") {
const filter =
categoryFilter?.length > 0
? categoryFilter?.sort().join(",")
: categorie || "";
const name = title == undefined ? "" : title;
const format = formatFilter == undefined ? "" : formatFilter?.join(",");
const filterGroup = group == undefined ? asPath.split("/")[2] : group;
loading();
const response = await listDataAll(
"",
name,
filter,
"",
tag,
filterGroup,
startDateString,
endDateString,
monthYearFilter
? getOnlyMonthAndYear(monthYearFilter)
?.split("/")[0]
?.replace("0", "")
: "",
monthYearFilter
? getOnlyMonthAndYear(monthYearFilter)?.split("/")[1]
: ""
);
close();
// setGetTotalPage(response?.data?.data?.totalPages);
// setContentImage(response?.data?.data?.content);
// setTotalContent(response?.data?.data?.totalElements);
const data = response?.data?.data;
const contentData = data?.content;
setImageData(contentData);
setTotalData(data?.totalElements);
setContentAll(response?.data?.data?.content);
setTotalPage(response?.data?.data?.totalPages);
setTotalContent(response?.data?.data?.totalElements);
}
} else {
const filter =
categoryFilter?.length > 0
? categoryFilter?.sort().join(",")
: categorie || "";
const name = title == undefined ? "" : title;
const format = formatFilter == undefined ? "" : formatFilter?.join(",");
loading();
const response = await listDataAll(
"",
name,
filter,
"",
"",
tag,
startDateString,
endDateString,
monthYearFilter
? getOnlyMonthAndYear(monthYearFilter)
?.split("/")[0]
?.replace("0", "")
: "",
monthYearFilter
? getOnlyMonthAndYear(monthYearFilter)?.split("/")[1]
: ""
);
close();
// setGetTotalPage(response?.data?.data?.totalPages);
// setContentImage(response?.data?.data?.content);
// setTotalContent(response?.data?.data?.totalElements);
const data = response?.data?.data;
const contentData = data?.content;
setImageData(contentData);
setTotalData(data?.totalElements);
setContentAll(response?.data?.data?.content);
setTotalPage(response?.data?.data?.totalPages);
setTotalContent(response?.data?.data?.totalElements);
}
}
const handleCategoryFilter = (e: boolean, id: string) => {
let filter = [...categoryFilter];
if (e) {
filter = [...categoryFilter, String(id)];
} else {
filter.splice(categoryFilter.indexOf(id), 1);
}
console.log("checkbox filter", filter);
setCategoryFilter(filter);
router.push(`?category=${filter.join("&")}`);
};
const handleFormatFilter = (e: boolean, id: string) => {
let filter = [...formatFilter];
if (e) {
filter = [...formatFilter, id];
} else {
filter.splice(formatFilter.indexOf(id), 1);
}
console.log("Format filter", filter);
setFormatFilter(filter);
};
const cleanCheckbox = () => {
setCategoryFilter([]);
setFormatFilter([]);
router.push(`?category=&title=`);
setDateRange([null, null]);
setMonthYearFilter(null);
setChange(!change);
};
async function getDataRegional() {
const filter =
categoryFilter?.length > 0
? categoryFilter?.sort().join(",")
: categorie || "";
const name = title == undefined ? "" : title;
const format = formatFilter == undefined ? "" : formatFilter?.join(",");
loading();
const response = await listDataRegional(
"",
name,
filter,
format,
"",
startDateString,
endDateString,
monthYearFilter
? getOnlyMonthAndYear(monthYearFilter)?.split("/")[0]?.replace("", "")
: "",
monthYearFilter
? getOnlyMonthAndYear(monthYearFilter)?.split("/")[1]
: "",
12,
pages,
sortByOpt
);
close();
setGetTotalPage(response?.data?.data?.totalPages);
setContentImage(response?.data?.data?.content);
setTotalContent(response?.data?.data?.totalElements);
}
function getSelectedCategory() {
const filter = [];
if (categorie) {
const categoryArr = categorie.split(",");
for (const element of categoryArr) {
filter.push(Number(element));
}
setCategoryFilter(filter);
}
}
const handleDeleteDate = () => {
setDateRange([null, null]);
setStartDateString("");
setEndDateString("");
setHandleClose(false);
};
const handleSorting = (e: any) => {
console.log(e.target.value);
if (e.target.value == "terbaru") {
setSortByOpt("createdAt");
} else {
setSortByOpt("clickCount");
}
setChange(!change);
};
async function getUserLevels() {
const res = await getUserLevelListByParent(761);
const userLevelList = res?.data?.data;
if (userLevelList !== null) {
let optionArr: any = [];
userLevelList?.map((option: any) => {
let optionData = {
id: option.id,
label: option.name,
value: option.id,
};
optionArr.push(optionData);
});
setUserLevels(optionArr);
}
}
const handleKeyUp = () => {
clearTimeout(typingTimer);
typingTimer = setTimeout(doneTyping, doneTypingInterval);
};
async function doneTyping() {
if (searchTitle == "" || searchTitle == undefined) {
router.push("?title=");
} else {
router.push(`?title=${searchTitle}`);
}
}
const handleKeyDown = () => {
clearTimeout(typingTimer);
};
return (
<div className="flex flex-col w-full max-w-screen overflow-x-hidden">
<div className="flex flex-row md:flex-row items-start gap-3 py-10 px-4 lg:px-20 bg-[#f7f7f7] dark:bg-black">
<p> {t("content", { defaultValue: "Content" })}</p>
{">"}
<p>
<span className="font-bold">
{t("allContent", { defaultValue: "All Content" })}
</span>
</p>
<p className="font-bold">|</p>
<p>{`${t("thereIs", { defaultValue: "There Is" })} ${totalContent} ${t(
"downloadableContent",
{ defaultValue: "Downloadable ContentS" }
)}`}</p>
</div>
{/* Left */}
<div className="flex flex-col lg:flex-row gap-6 pl-4 lg:pl-20 py-4">
<div className="lg:hidden flex justify-end mb-2">
<button
onClick={() => setIsFilterOpen(!isFilterOpen)}
className="text-sm text-white bg-[#bb3523] px-4 py-1 rounded-md shadow"
>
{isFilterOpen ? "Hide Filter" : "Show Filter"}
</button>
</div>
{isFilterOpen && (
<div className="h-fit min-w-full lg:min-w-[280px] max-w-full lg:max-w-[300px] bg-[#f7f7f7] dark:bg-black p-4 rounded-lg shadow-md">
<h2 className="text-lg font-semibold mb-4 flex items-center gap-1">
<Icon icon="stash:filter-light" fontSize={30} />
Filter
</h2>
<div className="border-t border-black my-4 dark:border-white"></div>
<div className="space-y-6">
<div>
<label
htmlFor="search"
className="block text-sm font-medium text-gray-700 dark:text-white"
>
{t("search", { defaultValue: "Search" })}
</label>
<Input
value={searchTitle}
onChange={(e) => setSearchTitle(e.target.value)}
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
type="text"
id="search"
placeholder={t("searchTitle", {
defaultValue: "Search Title",
})}
className="mt-1 w-full border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-white">
{t("monthYear", { defaultValue: "Month Year" })}
</label>
<ReactDatePicker
selected={monthYearFilter}
className="mt-1 w-full text-xs border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500"
onChange={(date) => setMonthYearFilter(date)}
dateFormat="MM | yyyy"
placeholderText={t("selectYear", {
defaultValue: "Select Year",
})}
showMonthYearPicker
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-white">
{t("date", { defaultValue: "Date" })}
</label>
<div className="flex flex-row justify justify-between gap-2">
<ReactDatePicker
selectsRange
className="mt-1 w-full border text-sm rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500"
startDate={dateRange[0]}
endDate={dateRange[1]}
onChange={(update) => {
setDateRange(update);
}}
placeholderText={t("selectDate", {
defaultValue: "Select Date",
})}
onCalendarClose={() => setCalenderState(!calenderState)}
/>
<div className="flex items-center">
{handleClose ? (
<Icon
icon="carbon:close-filled"
onClick={handleDeleteDate}
width="20"
inline
color="#216ba5"
/>
) : (
""
)}
</div>
</div>
</div>
<div>
<h3 className="text-sm font-medium text-gray-700 dark:text-white">
{t("categories", { defaultValue: "Categories" })}
</h3>
<ul className="mt-2 space-y-2">
{categories.map((category: any) => (
<li key={category?.id}>
<label
className="inline-flex items-center"
htmlFor={`${category.id}`}
>
<Checkbox
id={`${category.id}`}
value={category.id}
checked={categoryFilter.includes(String(category.id))}
onCheckedChange={(e) =>
handleCategoryFilter(Boolean(e), category.id)
}
/>
<span className="ml-2 text-gray-700 dark:text-white">
{category?.name}
</span>
</label>
</li>
))}
<div className="mt-4 flex gap-2 justify-center items-center">
<button
onClick={() =>
setCategoryPage((prev) => Math.max(prev - 1, 1))
}
disabled={categoryPage === 1}
className="px-3 py-1 border rounded disabled:opacity-50"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="m13.15 16.15l-3.625-3.625q-.125-.125-.175-.25T9.3 12t.05-.275t.175-.25L13.15 7.85q.075-.075.163-.112T13.5 7.7q.2 0 .35.138T14 8.2v7.6q0 .225-.15.363t-.35.137q-.05 0-.35-.15"
/>
</svg>
</button>
{Array.from({ length: categoryTotalPages }, (_, i) => (
<button
key={i}
onClick={() => setCategoryPage(i + 1)}
className={`px-3 py-1 border rounded ${
categoryPage === i + 1
? "bg-[#bb3523] text-white"
: ""
}`}
>
{i + 1}
</button>
))}
<button
onClick={() =>
setCategoryPage((prev) =>
Math.min(prev + 1, categoryTotalPages)
)
}
disabled={categoryPage === categoryTotalPages}
className="px-3 py-1 border rounded disabled:opacity-50"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M10.5 16.3q-.2 0-.35-.137T10 15.8V8.2q0-.225.15-.362t.35-.138q.05 0 .35.15l3.625 3.625q.125.125.175.25t.05.275t-.05.275t-.175.25L10.85 16.15q-.075.075-.162.113t-.188.037"
/>
</svg>
</button>
</div>
</ul>
</div>
{/* Garis */}
<div className="border-t border-black my-4 dark:border-white"></div>
{/* Garis */}
<div>
<h3 className="text-sm font-medium text-gray-700 dark:text-white">
Format
</h3>
<ul className="mt-2 space-y-2">
<li>
<label className="inline-flex items-center">
<Checkbox
id="png"
value="png"
checked={formatFilter.includes("png")}
onCheckedChange={(e) =>
handleFormatFilter(Boolean(e), "png")
}
/>
<span className="ml-2 text-gray-700 dark:text-white">
PNG
</span>
</label>
</li>
<li>
<label className="inline-flex items-center">
<Checkbox
id="jpeg"
value="jpeg"
checked={formatFilter.includes("jpeg")}
onCheckedChange={(e) =>
handleFormatFilter(Boolean(e), "jpeg")
}
/>
<span className="ml-2 text-gray-700 dark:text-white">
JPEG
</span>
</label>
</li>
<li>
<label className="inline-flex items-center">
<Checkbox
id="jpg"
value="jpg"
checked={formatFilter.includes("jpg")}
onCheckedChange={(e) =>
handleFormatFilter(Boolean(e), "jpg")
}
/>
<span className="ml-2 text-gray-700 dark:text-white">
JPG
</span>
</label>
</li>
</ul>
</div>
<div className="border-t border-black dark:border-white my-4"></div>
<div className="text-center">
<a
onClick={cleanCheckbox}
className="text-[#bb3523] cursor-pointer"
>
<b>Reset Filter</b>
</a>
</div>
</div>
</div>
)}
{/* Konten Kanan */}
<div className="w-full pl-4 pr-4 lg:pr-16 pb-4 overflow-x-hidden">
<div className="w-full">
<div className="flex flex-col items-end mb-4">
<h2 className="text-lg font-semibold">
{t("sortBy", { defaultValue: "Sort By" })}
</h2>
<select
defaultValue={sortBy == "popular" ? "terpopuler" : "terbaru"}
onChange={(e) => handleSorting(e)}
className="border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500"
>
<option value="latest">
{t("latest", { defaultValue: "Latest" })}
</option>
<option value="popular">
{t("mostPopular", { defaultValue: "Most Popular" })}
</option>
</select>
</div>
<div className="flex flex-col gap-4 w-full">
<FilterImageComponent
categoryFilter={categoryFilter}
sortByOpt={sortByOpt}
startDateString={startDateString}
endDateString={endDateString}
monthYearFilter={monthYearFilter}
/>
<FilterVideoComponent
categoryFilter={categoryFilter}
sortByOpt={sortByOpt}
startDateString={startDateString}
endDateString={endDateString}
monthYearFilter={monthYearFilter}
/>
<FilterDocumentComponent
categoryFilter={categoryFilter}
sortByOpt={sortByOpt}
startDateString={startDateString}
endDateString={endDateString}
monthYearFilter={monthYearFilter}
/>
<FilterAudioComponent
categoryFilter={categoryFilter}
sortByOpt={sortByOpt}
startDateString={startDateString}
endDateString={endDateString}
monthYearFilter={monthYearFilter}
/>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,14 @@
import Footer from "@/components/landing-page/footer";
import Navbar from "@/components/landing-page/navbar";
const layout = async ({ children }: { children: React.ReactNode }) => {
return (
<>
<Navbar />
{children}
<Footer />
</>
);
};
export default layout;

View File

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

View File

@ -6,6 +6,7 @@ import { Icon } from "@iconify/react/dist/iconify.js";
import { getOnlyDate, getOnlyMonthAndYear } from "@/utils/globals"; import { getOnlyDate, getOnlyMonthAndYear } from "@/utils/globals";
import { useParams, usePathname, useSearchParams } from "next/navigation"; import { useParams, usePathname, useSearchParams } from "next/navigation";
import { import {
getPublicCategoryData,
getUserLevelListByParent, getUserLevelListByParent,
listCategory, listCategory,
listData, listData,
@ -18,13 +19,7 @@ import {
PaginationState, PaginationState,
SortingState, SortingState,
VisibilityState, VisibilityState,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import { Reveal } from "@/components/landing-page/Reveal";
import { Link, useRouter } from "@/i18n/routing"; import { Link, useRouter } from "@/i18n/routing";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import ReactDatePicker from "react-datepicker"; import ReactDatePicker from "react-datepicker";
@ -34,6 +29,7 @@ import FilterImageComponent from "@/components/landing-page/filter-all/image-fil
import FilterVideoComponent from "@/components/landing-page/filter-all/video-filter-card"; import FilterVideoComponent from "@/components/landing-page/filter-all/video-filter-card";
import FilterDocumentComponent from "@/components/landing-page/filter-all/document-filter-card"; import FilterDocumentComponent from "@/components/landing-page/filter-all/document-filter-card";
import FilterAudioComponent from "@/components/landing-page/filter-all/audio-filter-card"; import FilterAudioComponent from "@/components/landing-page/filter-all/audio-filter-card";
import { useTranslations } from "next-intl";
export default function FilterPage() { export default function FilterPage() {
const router = useRouter(); const router = useRouter();
@ -86,8 +82,52 @@ export default function FilterPage() {
const [categories, setCategories] = useState([]); const [categories, setCategories] = useState([]);
const [userLevels, setUserLevels] = useState([]); const [userLevels, setUserLevels] = useState([]);
const [contentAll, setContentAll] = useState([]); const [contentAll, setContentAll] = useState([]);
const t = useTranslations("FilterPage");
const [isFilterOpen, setIsFilterOpen] = useState(true);
// const [startDate, endDate] = dateRange; const [categoryPage, setCategoryPage] = useState(1);
const [categoryTotalPages, setCategoryTotalPages] = useState(1);
const poldaName = params?.polda_name;
const satkerName = params?.satker_name;
useEffect(() => {
const title = searchParams?.get("title") || "";
const category = searchParams?.get("category") || "";
const sortBy = searchParams?.get("sortBy") || "latest";
setSearchTitle(title);
setCategoryFilter(category ? category.split("&") : []);
setSortByOpt(sortBy === "popular" ? "clickCount" : "createdAt");
}, [searchParams]);
useEffect(() => {
fetchCategories(categoryPage);
}, [categoryPage]);
const fetchCategories = async (pageNumber: number) => {
const groupParam =
poldaName && poldaName.length > 1
? poldaName
: satkerName && satkerName.length > 1
? "satker-" + satkerName
: "";
const isInt = locale === "en";
const response = await getPublicCategoryData(
groupParam,
"",
isInt,
pageNumber
);
const content = response?.data?.data?.content || [];
const total = response?.data?.data?.totalPages || 1;
setCategories(content);
setCategoryTotalPages(total);
};
React.useEffect(() => { React.useEffect(() => {
const pageFromUrl = searchParams?.get("page"); const pageFromUrl = searchParams?.get("page");
@ -387,244 +427,314 @@ export default function FilterPage() {
}; };
return ( return (
<div className="flex flex-col"> <div className="flex flex-col w-full max-w-screen overflow-x-hidden">
{/* Header */} <div className="flex flex-row md:flex-row items-start gap-3 py-10 px-4 lg:px-20 bg-[#f7f7f7] dark:bg-black">
<p> {t("content", { defaultValue: "Content" })}</p>
<div className="flex flex-col md:flex-row items-start gap-5 p-10 bg-[#f7f7f7] dark:bg-black"> {">"}
<p> <p>
{" "} <span className="font-bold">
Konten {">"} <span className="font-bold">Semua Konten</span> {t("allContent", { defaultValue: "All Content" })}
</span>
</p> </p>
<p className="font-bold">|</p> <p className="font-bold">|</p>
<p>{`Hasil Pencarian ${title} `}</p> <p>{`${t("thereIs", { defaultValue: "There Is" })} ${totalContent} ${t(
{`Terdapat ${contentAll?.length} konten yang dapat diunduh`} "downloadableContent",
{ defaultValue: "Downloadable ContentS" }
)}`}</p>
</div> </div>
{/* Left */} {/* Left */}
<div className="flex flex-col lg:flex-row gap-6 p-4"> <div className="flex flex-col lg:flex-row gap-6 pl-4 lg:pl-20 py-4">
<div className="lg:w-[25%] w-full bg-[#f7f7f7] dark:bg-black p-4 rounded-lg shadow-md"> <div className="lg:hidden flex justify-end mb-2">
<h2 className="text-lg font-semibold mb-4 flex items-center gap-1"> <button
<Icon icon="stash:filter-light" fontSize={30} /> onClick={() => setIsFilterOpen(!isFilterOpen)}
Filter className="text-sm text-white bg-[#bb3523] px-4 py-1 rounded-md shadow"
</h2> >
<div className="border-t border-black my-4 dark:border-white"></div> {isFilterOpen ? "Hide Filter" : "Show Filter"}
<div className="space-y-6"> </button>
<div> </div>
<label
htmlFor="search"
className="block text-sm font-medium text-gray-700 dark:text-white"
>
Pencarian
</label>
<Input
value={searchTitle}
onChange={(e) => setSearchTitle(e.target.value)}
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
type="text"
id="search"
placeholder="Cari judul..."
className="mt-1 w-full border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500"
/>
</div>
<div> {isFilterOpen && (
<label className="block text-sm font-medium text-gray-700 dark:text-white"> <div className="h-fit min-w-full lg:min-w-[280px] max-w-full lg:max-w-[300px] bg-[#f7f7f7] dark:bg-black p-4 rounded-lg shadow-md">
Tahun & Bulan <h2 className="text-lg font-semibold mb-4 flex items-center gap-1">
</label> <Icon icon="stash:filter-light" fontSize={30} />
<ReactDatePicker Filter
selected={monthYearFilter} </h2>
className="mt-1 w-full border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500" <div className="border-t border-black my-4 dark:border-white"></div>
onChange={(date) => setMonthYearFilter(date)} <div className="space-y-6">
dateFormat="MM | yyyy" <div>
placeholderText="Pilih Tahun dan Bulan" <label
showMonthYearPicker htmlFor="search"
/> className="block text-sm font-medium text-gray-700 dark:text-white"
</div> >
{t("search", { defaultValue: "Search" })}
<div> </label>
<label className="block text-sm font-medium text-gray-700 dark:text-white"> <Input
Tanggal value={searchTitle}
</label> onChange={(e) => setSearchTitle(e.target.value)}
<div className="flex flex-row justify justify-between gap-2"> onKeyUp={handleKeyUp}
<ReactDatePicker onKeyDown={handleKeyDown}
selectsRange type="text"
id="search"
placeholder={t("searchTitle", {
defaultValue: "Search Title",
})}
className="mt-1 w-full border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500" className="mt-1 w-full border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500"
startDate={dateRange[0]}
endDate={dateRange[1]}
onChange={(update) => {
setDateRange(update);
}}
placeholderText="Pilih Tanggal"
onCalendarClose={() => setCalenderState(!calenderState)}
/> />
<div className="flex items-center"> </div>
{handleClose ? (
<Icon <div>
icon="carbon:close-filled" <label className="block text-sm font-medium text-gray-700 dark:text-white">
onClick={handleDeleteDate} {t("monthYear", { defaultValue: "Month Year" })}
width="20" </label>
inline <ReactDatePicker
color="#216ba5" selected={monthYearFilter}
/> className="mt-1 w-full text-xs border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500"
) : ( onChange={(date) => setMonthYearFilter(date)}
"" dateFormat="MM | yyyy"
)} placeholderText={t("selectYear", {
defaultValue: "Select Year",
})}
showMonthYearPicker
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-white">
{t("date", { defaultValue: "Date" })}
</label>
<div className="flex flex-row justify justify-between gap-2">
<ReactDatePicker
selectsRange
className="mt-1 w-full border text-sm rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500"
startDate={dateRange[0]}
endDate={dateRange[1]}
onChange={(update) => {
setDateRange(update);
}}
placeholderText={t("selectDate", {
defaultValue: "Select Date",
})}
onCalendarClose={() => setCalenderState(!calenderState)}
/>
<div className="flex items-center">
{handleClose ? (
<Icon
icon="carbon:close-filled"
onClick={handleDeleteDate}
width="20"
inline
color="#216ba5"
/>
) : (
""
)}
</div>
</div> </div>
</div> </div>
</div>
<div> <div>
<h3 className="text-sm font-medium text-gray-700 dark:text-white"> <h3 className="text-sm font-medium text-gray-700 dark:text-white">
Kategori {t("categories", { defaultValue: "Categories" })}
</h3> </h3>
<ul className="mt-2 space-y-2"> <ul className="mt-2 space-y-2">
{categories.map((category: any) => ( {categories.map((category: any) => (
<li key={category?.id}> <li key={category?.id}>
<label <label
className="inline-flex items-center" className="inline-flex items-center"
htmlFor={`${category.id}`} htmlFor={`${category.id}`}
>
<Checkbox
id={`${category.id}`}
value={category.id}
checked={categoryFilter.includes(String(category.id))}
onCheckedChange={(e) =>
handleCategoryFilter(Boolean(e), category.id)
}
/>
<span className="ml-2 text-gray-700 dark:text-white">
{category?.name}
</span>
</label>
</li>
))}
<div className="mt-4 flex gap-2 justify-center items-center">
<button
onClick={() =>
setCategoryPage((prev) => Math.max(prev - 1, 1))
}
disabled={categoryPage === 1}
className="px-3 py-1 border rounded disabled:opacity-50"
> >
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="m13.15 16.15l-3.625-3.625q-.125-.125-.175-.25T9.3 12t.05-.275t.175-.25L13.15 7.85q.075-.075.163-.112T13.5 7.7q.2 0 .35.138T14 8.2v7.6q0 .225-.15.363t-.35.137q-.05 0-.35-.15"
/>
</svg>
</button>
{Array.from({ length: categoryTotalPages }, (_, i) => (
<button
key={i}
onClick={() => setCategoryPage(i + 1)}
className={`px-3 py-1 border rounded ${
categoryPage === i + 1
? "bg-[#bb3523] text-white"
: ""
}`}
>
{i + 1}
</button>
))}
<button
onClick={() =>
setCategoryPage((prev) =>
Math.min(prev + 1, categoryTotalPages)
)
}
disabled={categoryPage === categoryTotalPages}
className="px-3 py-1 border rounded disabled:opacity-50"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M10.5 16.3q-.2 0-.35-.137T10 15.8V8.2q0-.225.15-.362t.35-.138q.05 0 .35.15l3.625 3.625q.125.125.175.25t.05.275t-.05.275t-.175.25L10.85 16.15q-.075.075-.162.113t-.188.037"
/>
</svg>
</button>
</div>
</ul>
</div>
{/* Garis */}
<div className="border-t border-black my-4 dark:border-white"></div>
{/* Garis */}
<div>
<h3 className="text-sm font-medium text-gray-700 dark:text-white">
Format
</h3>
<ul className="mt-2 space-y-2">
<li>
<label className="inline-flex items-center">
<Checkbox <Checkbox
id={`${category.id}`} id="png"
value={category.id} value="png"
checked={categoryFilter.includes(String(category.id))} checked={formatFilter.includes("png")}
onCheckedChange={(e) => onCheckedChange={(e) =>
handleCategoryFilter(Boolean(e), category.id) handleFormatFilter(Boolean(e), "png")
} }
/> />
<span className="ml-2 text-gray-700 dark:text-white"> <span className="ml-2 text-gray-700 dark:text-white">
{category?.name} PNG
</span> </span>
</label> </label>
</li> </li>
))} <li>
</ul> <label className="inline-flex items-center">
</div> <Checkbox
{/* Garis */} id="jpeg"
<div className="border-t border-black my-4 dark:border-white"></div> value="jpeg"
{/* Garis */} checked={formatFilter.includes("jpeg")}
<div> onCheckedChange={(e) =>
<h3 className="text-sm font-medium text-gray-700 dark:text-white"> handleFormatFilter(Boolean(e), "jpeg")
Format Konten }
</h3> />
<ul className="mt-2 space-y-2"> <span className="ml-2 text-gray-700 dark:text-white">
<li> JPEG
<label className="inline-flex items-center"> </span>
<Checkbox </label>
id="png" </li>
value="png" <li>
checked={formatFilter.includes("image")} <label className="inline-flex items-center">
onCheckedChange={(e) => <Checkbox
handleFormatFilter(Boolean(e), "image") id="jpg"
} value="jpg"
/> checked={formatFilter.includes("jpg")}
<span className="ml-2 text-gray-700 dark:text-white"> onCheckedChange={(e) =>
Foto handleFormatFilter(Boolean(e), "jpg")
</span> }
</label> />
</li> <span className="ml-2 text-gray-700 dark:text-white">
<li> JPG
<label className="inline-flex items-center"> </span>
<Checkbox </label>
id="jpeg" </li>
value="jpeg" </ul>
checked={formatFilter.includes("video")} </div>
onCheckedChange={(e) => <div className="border-t border-black dark:border-white my-4"></div>
handleFormatFilter(Boolean(e), "video") <div className="text-center">
} <a
/> onClick={cleanCheckbox}
<span className="ml-2 text-gray-700 dark:text-white"> className="text-[#bb3523] cursor-pointer"
Audio Visual >
</span> <b>Reset Filter</b>
</label> </a>
</li> </div>
<li>
<label className="inline-flex items-center">
<Checkbox
id="jpg"
value="jpg"
checked={formatFilter.includes("document")}
onCheckedChange={(e) =>
handleFormatFilter(Boolean(e), "document")
}
/>
<span className="ml-2 text-gray-700 dark:text-white">
Teks
</span>
</label>
</li>
<li>
<label className="inline-flex items-center">
<Checkbox
id="jpg"
value="jpg"
checked={formatFilter.includes("audio")}
onCheckedChange={(e) =>
handleFormatFilter(Boolean(e), "audio")
}
/>
<span className="ml-2 text-gray-700 dark:text-white">
Audio
</span>
</label>
</li>
</ul>
</div>
<div className="border-t border-black dark:border-white my-4"></div>
<div className="text-center">
<a
onClick={cleanCheckbox}
className="text-[#bb3523] cursor-pointer"
>
<b>Reset Filter</b>
</a>
</div> </div>
</div> </div>
</div> )}
{/* Konten Kanan */} {/* Konten Kanan */}
<div className="flex-1 w-[75%]"> <div className="w-full pl-4 pr-4 lg:pr-16 pb-4 overflow-x-hidden">
<div className="flex flex-col items-end mb-4"> <div className="w-full">
<h2 className="text-lg font-semibold">Urutkan berdasarkan</h2> <div className="flex flex-col items-end mb-4">
<select <h2 className="text-lg font-semibold">
defaultValue={sortBy == "popular" ? "terpopuler" : "terbaru"} {t("sortBy", { defaultValue: "Sort By" })}
onChange={(e) => handleSorting(e)} </h2>
className="border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500" <select
> defaultValue={sortBy == "popular" ? "terpopuler" : "terbaru"}
<option value="terbaru">Terbaru</option> onChange={(e) => handleSorting(e)}
<option value="terpopuler">Terpopuler</option> className="border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500"
</select> >
</div> <option value="latest">
<div className="flex flex-col gap-2 w-full"> {t("latest", { defaultValue: "Latest" })}
<FilterImageComponent </option>
categoryFilter={categoryFilter} <option value="popular">
sortByOpt={sortByOpt} {t("mostPopular", { defaultValue: "Most Popular" })}
startDateString={startDateString} </option>
endDateString={endDateString} </select>
monthYearFilter={monthYearFilter} </div>
/> <div className="flex flex-col gap-4 w-full">
<FilterVideoComponent <FilterImageComponent
categoryFilter={categoryFilter} categoryFilter={categoryFilter}
sortByOpt={sortByOpt} sortByOpt={sortByOpt}
startDateString={startDateString} startDateString={startDateString}
endDateString={endDateString} endDateString={endDateString}
monthYearFilter={monthYearFilter} monthYearFilter={monthYearFilter}
/> />
<FilterDocumentComponent <FilterVideoComponent
categoryFilter={categoryFilter} categoryFilter={categoryFilter}
sortByOpt={sortByOpt} sortByOpt={sortByOpt}
startDateString={startDateString} startDateString={startDateString}
endDateString={endDateString} endDateString={endDateString}
monthYearFilter={monthYearFilter} monthYearFilter={monthYearFilter}
/> />
<FilterAudioComponent <FilterDocumentComponent
categoryFilter={categoryFilter} categoryFilter={categoryFilter}
sortByOpt={sortByOpt} sortByOpt={sortByOpt}
startDateString={startDateString} startDateString={startDateString}
endDateString={endDateString} endDateString={endDateString}
monthYearFilter={monthYearFilter} monthYearFilter={monthYearFilter}
/> />
<FilterAudioComponent
categoryFilter={categoryFilter}
sortByOpt={sortByOpt}
startDateString={startDateString}
endDateString={endDateString}
monthYearFilter={monthYearFilter}
/>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -488,10 +488,16 @@ const FilterPage = () => {
<div className="flex flex-col md:flex-row items-start gap-5 py-10 px-4 lg:px-20 bg-[#f7f7f7] dark:bg-black"> <div className="flex flex-col md:flex-row items-start gap-5 py-10 px-4 lg:px-20 bg-[#f7f7f7] dark:bg-black">
<p> <p>
{" "} {" "}
Audio {">"} <span className="font-bold">{t("allAudio", { defaultValue: "All Audio" })}</span> Audio {">"}{" "}
<span className="font-bold">
{t("allAudio", { defaultValue: "All Audio" })}
</span>
</p> </p>
<p className="font-bold">|</p> <p className="font-bold">|</p>
<p>{`${t("thereIs", { defaultValue: "There Is" })} ${totalContent} ${t("downloadableAudio", { defaultValue: "Downloadable Audio" })}`}</p> <p>{`${t("thereIs", { defaultValue: "There Is" })} ${totalContent} ${t(
"downloadableAudio",
{ defaultValue: "Downloadable Audio" }
)}`}</p>
</div> </div>
{/* Left */} {/* Left */}
@ -531,7 +537,9 @@ const FilterPage = () => {
className="mt-1 w-full text-xs border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500" className="mt-1 w-full text-xs border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500"
onChange={(date) => setMonthYearFilter(date)} onChange={(date) => setMonthYearFilter(date)}
dateFormat="MM | yyyy" dateFormat="MM | yyyy"
placeholderText={t("selectYear", { defaultValue: "Select Year" })} placeholderText={t("selectYear", {
defaultValue: "Select Year",
})}
showMonthYearPicker showMonthYearPicker
/> />
</div> </div>
@ -549,7 +557,9 @@ const FilterPage = () => {
onChange={(update) => { onChange={(update) => {
setDateRange(update); setDateRange(update);
}} }}
placeholderText={t("searchDate", { defaultValue: "Search Date" })} placeholderText={t("searchDate", {
defaultValue: "Search Date",
})}
onCalendarClose={() => setCalenderState(!calenderState)} onCalendarClose={() => setCalenderState(!calenderState)}
/> />
<div className="flex items-center"> <div className="flex items-center">
@ -594,7 +604,6 @@ const FilterPage = () => {
</li> </li>
))} ))}
<div className="mt-4 flex gap-2 justify-center items-center"> <div className="mt-4 flex gap-2 justify-center items-center">
{/* Tombol Prev */}
<button <button
onClick={() => onClick={() =>
setCategoryPage((prev) => Math.max(prev - 1, 1)) setCategoryPage((prev) => Math.max(prev - 1, 1))
@ -615,7 +624,6 @@ const FilterPage = () => {
</svg> </svg>
</button> </button>
{/* Nomor Halaman */}
{Array.from({ length: categoryTotalPages }, (_, i) => ( {Array.from({ length: categoryTotalPages }, (_, i) => (
<button <button
key={i} key={i}
@ -628,7 +636,6 @@ const FilterPage = () => {
</button> </button>
))} ))}
{/* Tombol Next */}
<button <button
onClick={() => onClick={() =>
setCategoryPage((prev) => setCategoryPage((prev) =>
@ -709,14 +716,20 @@ const FilterPage = () => {
<div className="w-full pr-4 lg:pr-16 pb-4"> <div className="w-full pr-4 lg:pr-16 pb-4">
<Reveal> <Reveal>
<div className="flex flex-col items-end mb-4"> <div className="flex flex-col items-end mb-4">
<h2 className="text-lg font-semibold">{t("sortBy", { defaultValue: "Sort By" })} </h2> <h2 className="text-lg font-semibold">
{t("sortBy", { defaultValue: "Sort By" })}{" "}
</h2>
<select <select
defaultValue={sortBy == "popular" ? "terpopuler" : "terbaru"} defaultValue={sortBy == "popular" ? "terpopuler" : "terbaru"}
onChange={(e) => handleSorting(e)} onChange={(e) => handleSorting(e)}
className="border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500" className="border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500"
> >
<option value="latest">{t("latest", { defaultValue: "Latest" })}</option> <option value="latest">
<option value="popular">{t("mostPopular", { defaultValue: "Most Popular" })}</option> {t("latest", { defaultValue: "Latest" })}
</option>
<option value="popular">
{t("mostPopular", { defaultValue: "Most Popular" })}
</option>
</select> </select>
</div> </div>

View File

@ -504,10 +504,16 @@ const FilterPage = () => {
<div className="flex flex-col md:flex-row items-start gap-5 py-10 px-4 lg:px-20 bg-[#f7f7f7] dark:bg-black"> <div className="flex flex-col md:flex-row items-start gap-5 py-10 px-4 lg:px-20 bg-[#f7f7f7] dark:bg-black">
<p> <p>
{" "} {" "}
{t("text", { defaultValue: "Text" })} {">"} <span className="font-bold">{t("allText", { defaultValue: "All Text" })}</span> {t("text", { defaultValue: "Text" })} {">"}{" "}
<span className="font-bold">
{t("allText", { defaultValue: "All Text" })}
</span>
</p> </p>
<p className="font-bold">|</p> <p className="font-bold">|</p>
<p>{`${t("thereIs", { defaultValue: "There Is" })} ${totalContent} ${t("downloadableText", { defaultValue: "Downloadable Text" })}`}</p> <p>{`${t("thereIs", { defaultValue: "There Is" })} ${totalContent} ${t(
"downloadableText",
{ defaultValue: "Downloadable Text" }
)}`}</p>
</div> </div>
{/* Left */} {/* Left */}
@ -547,7 +553,9 @@ const FilterPage = () => {
className="mt-1 w-full text-xs border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500" className="mt-1 w-full text-xs border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500"
onChange={(date) => setMonthYearFilter(date)} onChange={(date) => setMonthYearFilter(date)}
dateFormat="MM | yyyy" dateFormat="MM | yyyy"
placeholderText={t("selectYear", { defaultValue: "Select Year" })} placeholderText={t("selectYear", {
defaultValue: "Select Year",
})}
showMonthYearPicker showMonthYearPicker
/> />
</div> </div>
@ -565,7 +573,9 @@ const FilterPage = () => {
onChange={(update) => { onChange={(update) => {
setDateRange(update); setDateRange(update);
}} }}
placeholderText={t("selectDate", { defaultValue: "Select Date" })} placeholderText={t("selectDate", {
defaultValue: "Select Date",
})}
onCalendarClose={() => setCalenderState(!calenderState)} onCalendarClose={() => setCalenderState(!calenderState)}
/> />
<div className="flex items-center"> <div className="flex items-center">
@ -610,7 +620,6 @@ const FilterPage = () => {
</li> </li>
))} ))}
<div className="mt-4 flex gap-2 justify-center items-center"> <div className="mt-4 flex gap-2 justify-center items-center">
{/* Tombol Prev */}
<button <button
onClick={() => onClick={() =>
setCategoryPage((prev) => Math.max(prev - 1, 1)) setCategoryPage((prev) => Math.max(prev - 1, 1))
@ -631,7 +640,6 @@ const FilterPage = () => {
</svg> </svg>
</button> </button>
{/* Nomor Halaman */}
{Array.from({ length: categoryTotalPages }, (_, i) => ( {Array.from({ length: categoryTotalPages }, (_, i) => (
<button <button
key={i} key={i}
@ -644,7 +652,6 @@ const FilterPage = () => {
</button> </button>
))} ))}
{/* Tombol Next */}
<button <button
onClick={() => onClick={() =>
setCategoryPage((prev) => setCategoryPage((prev) =>
@ -771,14 +778,20 @@ const FilterPage = () => {
<Reveal> <Reveal>
<div className="flex-1"> <div className="flex-1">
<div className="flex flex-col items-end mb-4"> <div className="flex flex-col items-end mb-4">
<h2 className="text-lg font-semibold">{t("sortBy", { defaultValue: "Sort By" })}</h2> <h2 className="text-lg font-semibold">
{t("sortBy", { defaultValue: "Sort By" })}
</h2>
<select <select
defaultValue={sortBy == "popular" ? "terpopuler" : "terbaru"} defaultValue={sortBy == "popular" ? "terpopuler" : "terbaru"}
onChange={(e) => handleSorting(e)} onChange={(e) => handleSorting(e)}
className="border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500" className="border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500"
> >
<option value="latest">{t("latest", { defaultValue: "Latest" })}</option> <option value="latest">
<option value="popular">{t("mostPopular", { defaultValue: "Most Popular" })}</option> {t("latest", { defaultValue: "Latest" })}
</option>
<option value="popular">
{t("mostPopular", { defaultValue: "Most Popular" })}
</option>
</select> </select>
</div> </div>

View File

@ -485,24 +485,24 @@ const FilterPage = () => {
clearTimeout(typingTimer); clearTimeout(typingTimer);
}; };
const shimmer = (w: number, h: number) => ` // 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"> // <svg width="${w}" height="${h}" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs> // <defs>
<linearGradient id="g"> // <linearGradient id="g">
<stop stop-color="#bcbcbd" offset="20%" /> // <stop stop-color="#bcbcbd" offset="20%" />
<stop stop-color="#f9fafb" offset="50%" /> // <stop stop-color="#f9fafb" offset="50%" />
<stop stop-color="#bcbcbd" offset="70%" /> // <stop stop-color="#bcbcbd" offset="70%" />
</linearGradient> // </linearGradient>
</defs> // </defs>
<rect width="${w}" height="${h}" fill="#bcbcbd" /> // <rect width="${w}" height="${h}" fill="#bcbcbd" />
<rect id="r" width="${w}" height="${h}" fill="url(#g)" /> // <rect id="r" width="${w}" height="${h}" fill="url(#g)" />
<animate xlink:href="#r" attributeName="x" from="-${w}" to="${w}" dur="1s" repeatCount="indefinite" /> // <animate xlink:href="#r" attributeName="x" from="-${w}" to="${w}" dur="1s" repeatCount="indefinite" />
</svg>`; // </svg>`;
const toBase64 = (str: string) => // const toBase64 = (str: string) =>
typeof window === "undefined" // typeof window === "undefined"
? Buffer.from(str).toString("base64") // ? Buffer.from(str).toString("base64")
: window.btoa(str); // : window.btoa(str);
return ( return (
<div className="flex flex-col"> <div className="flex flex-col">
@ -511,14 +511,19 @@ const FilterPage = () => {
<p> {t("image", { defaultValue: "Image" })}</p> <p> {t("image", { defaultValue: "Image" })}</p>
{">"} {">"}
<p> <p>
<span className="font-bold">{t("allImage", { defaultValue: "All Image" })}</span> <span className="font-bold">
{t("allImage", { defaultValue: "All Image" })}
</span>
</p> </p>
<p className="font-bold">|</p> <p className="font-bold">|</p>
<p>{`${t("thereIs", { defaultValue: "There Is" })} ${totalContent} ${t("downloadableImage", { defaultValue: "Downloadable Image" })}`}</p> <p>{`${t("thereIs", { defaultValue: "There Is" })} ${totalContent} ${t(
"downloadableImage",
{ defaultValue: "Downloadable Image" }
)}`}</p>
</div> </div>
{/* Left */}
<div className="flex flex-col lg:flex-row gap-6 pl-4 lg:pl-20 py-4"> <div className="flex flex-col lg:flex-row gap-6 pl-4 lg:pl-20 py-4">
{/* Left */}
<div className="lg:hidden flex justify-end mb-2"> <div className="lg:hidden flex justify-end mb-2">
<button <button
onClick={() => setIsFilterOpen(!isFilterOpen)} onClick={() => setIsFilterOpen(!isFilterOpen)}
@ -550,7 +555,9 @@ const FilterPage = () => {
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
type="text" type="text"
id="search" id="search"
placeholder={t("searchTitle", { defaultValue: "Search Title" })} placeholder={t("searchTitle", {
defaultValue: "Search Title",
})}
className="mt-1 w-full border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500" className="mt-1 w-full border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500"
/> />
</div> </div>
@ -564,7 +571,9 @@ const FilterPage = () => {
className="mt-1 w-full text-xs border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500" className="mt-1 w-full text-xs border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500"
onChange={(date) => setMonthYearFilter(date)} onChange={(date) => setMonthYearFilter(date)}
dateFormat="MM | yyyy" dateFormat="MM | yyyy"
placeholderText={t("selectYear", { defaultValue: "Select Year" })} placeholderText={t("selectYear", {
defaultValue: "Select Year",
})}
showMonthYearPicker showMonthYearPicker
/> />
</div> </div>
@ -582,7 +591,9 @@ const FilterPage = () => {
onChange={(update) => { onChange={(update) => {
setDateRange(update); setDateRange(update);
}} }}
placeholderText={t("selectDate", { defaultValue: "Select Date" })} placeholderText={t("selectDate", {
defaultValue: "Select Date",
})}
onCalendarClose={() => setCalenderState(!calenderState)} onCalendarClose={() => setCalenderState(!calenderState)}
/> />
<div className="flex items-center"> <div className="flex items-center">
@ -627,7 +638,6 @@ const FilterPage = () => {
</li> </li>
))} ))}
<div className="mt-4 flex gap-2 justify-center items-center"> <div className="mt-4 flex gap-2 justify-center items-center">
{/* Tombol Prev */}
<button <button
onClick={() => onClick={() =>
setCategoryPage((prev) => Math.max(prev - 1, 1)) setCategoryPage((prev) => Math.max(prev - 1, 1))
@ -648,7 +658,6 @@ const FilterPage = () => {
</svg> </svg>
</button> </button>
{/* Nomor Halaman */}
{Array.from({ length: categoryTotalPages }, (_, i) => ( {Array.from({ length: categoryTotalPages }, (_, i) => (
<button <button
key={i} key={i}
@ -663,7 +672,6 @@ const FilterPage = () => {
</button> </button>
))} ))}
{/* Tombol Next */}
<button <button
onClick={() => onClick={() =>
setCategoryPage((prev) => setCategoryPage((prev) =>
@ -761,14 +769,20 @@ const FilterPage = () => {
<Reveal> <Reveal>
<div className="w-full"> <div className="w-full">
<div className="flex flex-col items-end mb-4"> <div className="flex flex-col items-end mb-4">
<h2 className="text-lg font-semibold">{t("sortBy", { defaultValue: "Sort By" })}</h2> <h2 className="text-lg font-semibold">
{t("sortBy", { defaultValue: "Sort By" })}
</h2>
<select <select
defaultValue={sortBy == "popular" ? "terpopuler" : "terbaru"} defaultValue={sortBy == "popular" ? "terpopuler" : "terbaru"}
onChange={(e) => handleSorting(e)} onChange={(e) => handleSorting(e)}
className="border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500" className="border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500"
> >
<option value="latest">{t("latest", { defaultValue: "Latest" })}</option> <option value="latest">
<option value="popular">{t("mostPopular", { defaultValue: "Most Popular" })}</option> {t("latest", { defaultValue: "Latest" })}
</option>
<option value="popular">
{t("mostPopular", { defaultValue: "Most Popular" })}
</option>
</select> </select>
</div> </div>

View File

@ -0,0 +1,731 @@
"use client";
import React, { useEffect, useState } from "react";
import { Checkbox } from "@/components/ui/checkbox";
import { Icon } from "@iconify/react/dist/iconify.js";
import { getOnlyDate, getOnlyMonthAndYear } from "@/utils/globals";
import { useParams, usePathname, useSearchParams } from "next/navigation";
import {
getPublicCategoryData,
getUserLevelListByParent,
listCategory,
listData,
listDataAll,
listDataRegional,
} from "@/service/landing/landing";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
} from "@tanstack/react-table";
import { Link, useRouter } from "@/i18n/routing";
import { Input } from "@/components/ui/input";
import ReactDatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
import { close, loading } from "@/config/swal";
import FilterImageComponent from "@/components/landing-page/filter-all/image-filter-card";
import FilterVideoComponent from "@/components/landing-page/filter-all/video-filter-card";
import FilterDocumentComponent from "@/components/landing-page/filter-all/document-filter-card";
import FilterAudioComponent from "@/components/landing-page/filter-all/audio-filter-card";
import { useTranslations } from "next-intl";
export default function FilterPage() {
const router = useRouter();
const asPath = usePathname();
const params = useParams();
const searchParams = useSearchParams();
const locale = params?.locale;
const [imageData, setImageData] = useState<any>();
const [totalData, setTotalData] = React.useState<number>(1);
const [totalPage, setTotalPage] = React.useState<number>(1);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const [page, setPage] = useState(1);
const [totalContent, setTotalContent] = useState();
const [change, setChange] = useState(false);
const sortBy = searchParams?.get("sortBy");
const title = searchParams?.get("title");
const categorie = searchParams?.get("category");
const group = searchParams?.get("group");
const tag: any = searchParams?.get("tag");
const [contentImage, setContentImage] = useState([]);
const [, setGetTotalPage] = useState();
let typingTimer: any;
const doneTypingInterval = 1500;
const [search, setSearch] = useState();
const [categoryFilter, setCategoryFilter] = useState<any>([]);
const [monthYearFilter, setMonthYearFilter] = useState<any>();
const [searchTitle, setSearchTitle] = useState<string>("");
const [sortByOpt, setSortByOpt] = useState<any>(
sortBy === "popular" ? "clickCount" : "createdAt"
);
const isRegional = asPath?.includes("regional");
const isSatker = asPath?.includes("satker");
const [formatFilter, setFormatFilter] = useState<any>([]);
const pages = page ? page - 1 : 0;
const [startDateString, setStartDateString] = useState<any>();
const [endDateString, setEndDateString] = useState<any>();
const [dateRange, setDateRange] = useState<any>([null, null]);
const [calenderState, setCalenderState] = useState(false);
const [handleClose, setHandleClose] = useState(false);
const [categories, setCategories] = useState([]);
const [userLevels, setUserLevels] = useState([]);
const [contentAll, setContentAll] = useState([]);
const t = useTranslations("FilterPage");
const [isFilterOpen, setIsFilterOpen] = useState(true);
const [categoryPage, setCategoryPage] = useState(1);
const [categoryTotalPages, setCategoryTotalPages] = useState(1);
const poldaName = params?.polda_name;
const satkerName = params?.satker_name;
useEffect(() => {
fetchCategories(categoryPage);
}, [categoryPage]);
const fetchCategories = async (pageNumber: number) => {
const groupParam =
poldaName && poldaName.length > 1
? poldaName
: satkerName && satkerName.length > 1
? "satker-" + satkerName
: "";
const isInt = locale === "en";
const response = await getPublicCategoryData(
groupParam,
"",
isInt,
pageNumber
); // halaman 1-based
const content = response?.data?.data?.content || [];
const total = response?.data?.data?.totalPages || 1;
setCategories(content);
setCategoryTotalPages(total);
};
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
useEffect(() => {
async function initState() {
getCategories();
// getSelectedCategory();
if (isSatker) {
getUserLevels();
}
}
initState();
}, []);
useEffect(() => {
if (categorie) {
setCategoryFilter(
categorie?.split("&")?.length > 1 ? categorie?.split("&") : [categorie]
);
console.log(
"Kategori",
categorie,
categorie?.split("&")?.length > 1 ? categorie?.split("&") : [categorie]
);
}
}, [categorie]);
// useEffect(() => {
// fetchData();
// }, [page, sortBy, sortByOpt, title]);
useEffect(() => {
async function initState() {
if (isRegional) {
getDataRegional();
} else {
getData();
}
}
console.log(monthYearFilter, "monthFilter");
initState();
}, [
change,
monthYearFilter,
sortBy,
sortByOpt,
title,
startDateString,
endDateString,
categorie,
formatFilter,
]);
async function getCategories() {
const category = await listCategory("1");
const resCategory = category?.data?.data?.content;
setCategories(resCategory);
}
useEffect(() => {
function initState() {
if (dateRange[0] != null && dateRange[1] != null) {
setStartDateString(getOnlyDate(dateRange[0]));
setEndDateString(getOnlyDate(dateRange[1]));
setHandleClose(true);
console.log("date range", dateRange, getOnlyDate(dateRange[0]));
}
}
initState();
}, [calenderState]);
async function getData() {
if (asPath?.includes("/polda/") == true) {
if (asPath?.split("/")[2] !== "[polda_name]") {
const filter =
categoryFilter?.length > 0
? categoryFilter?.sort().join(",")
: categorie || "";
const name = title == undefined ? "" : title;
const format = formatFilter == undefined ? "" : formatFilter?.join(",");
const filterGroup = group == undefined ? asPath.split("/")[2] : group;
loading();
const response = await listDataAll(
"",
name,
filter,
"",
tag,
filterGroup,
startDateString,
endDateString,
monthYearFilter
? getOnlyMonthAndYear(monthYearFilter)
?.split("/")[0]
?.replace("0", "")
: "",
monthYearFilter
? getOnlyMonthAndYear(monthYearFilter)?.split("/")[1]
: ""
);
close();
// setGetTotalPage(response?.data?.data?.totalPages);
// setContentImage(response?.data?.data?.content);
// setTotalContent(response?.data?.data?.totalElements);
const data = response?.data?.data;
const contentData = data?.content;
setImageData(contentData);
setTotalData(data?.totalElements);
setContentAll(response?.data?.data?.content);
setTotalPage(response?.data?.data?.totalPages);
setTotalContent(response?.data?.data?.totalElements);
}
} else {
const filter =
categoryFilter?.length > 0
? categoryFilter?.sort().join(",")
: categorie || "";
const name = title == undefined ? "" : title;
const format = formatFilter == undefined ? "" : formatFilter?.join(",");
loading();
const response = await listDataAll(
"",
name,
filter,
"",
"",
tag,
startDateString,
endDateString,
monthYearFilter
? getOnlyMonthAndYear(monthYearFilter)
?.split("/")[0]
?.replace("0", "")
: "",
monthYearFilter
? getOnlyMonthAndYear(monthYearFilter)?.split("/")[1]
: ""
);
close();
// setGetTotalPage(response?.data?.data?.totalPages);
// setContentImage(response?.data?.data?.content);
// setTotalContent(response?.data?.data?.totalElements);
const data = response?.data?.data;
const contentData = data?.content;
setImageData(contentData);
setTotalData(data?.totalElements);
setContentAll(response?.data?.data?.content);
setTotalPage(response?.data?.data?.totalPages);
setTotalContent(response?.data?.data?.totalElements);
}
}
const handleCategoryFilter = (e: boolean, id: string) => {
let filter = [...categoryFilter];
if (e) {
filter = [...categoryFilter, String(id)];
} else {
filter.splice(categoryFilter.indexOf(id), 1);
}
console.log("checkbox filter", filter);
setCategoryFilter(filter);
router.push(`?category=${filter.join("&")}`);
};
const handleFormatFilter = (e: boolean, id: string) => {
let filter = [...formatFilter];
if (e) {
filter = [...formatFilter, id];
} else {
filter.splice(formatFilter.indexOf(id), 1);
}
console.log("Format filter", filter);
setFormatFilter(filter);
};
const cleanCheckbox = () => {
setCategoryFilter([]);
setFormatFilter([]);
router.push(`?category=&title=`);
setDateRange([null, null]);
setMonthYearFilter(null);
setChange(!change);
};
async function getDataRegional() {
const filter =
categoryFilter?.length > 0
? categoryFilter?.sort().join(",")
: categorie || "";
const name = title == undefined ? "" : title;
const format = formatFilter == undefined ? "" : formatFilter?.join(",");
loading();
const response = await listDataRegional(
"",
name,
filter,
format,
"",
startDateString,
endDateString,
monthYearFilter
? getOnlyMonthAndYear(monthYearFilter)?.split("/")[0]?.replace("", "")
: "",
monthYearFilter
? getOnlyMonthAndYear(monthYearFilter)?.split("/")[1]
: "",
12,
pages,
sortByOpt
);
close();
setGetTotalPage(response?.data?.data?.totalPages);
setContentImage(response?.data?.data?.content);
setTotalContent(response?.data?.data?.totalElements);
}
function getSelectedCategory() {
const filter = [];
if (categorie) {
const categoryArr = categorie.split(",");
for (const element of categoryArr) {
filter.push(Number(element));
}
setCategoryFilter(filter);
}
}
const handleDeleteDate = () => {
setDateRange([null, null]);
setStartDateString("");
setEndDateString("");
setHandleClose(false);
};
const handleSorting = (e: any) => {
console.log(e.target.value);
if (e.target.value == "terbaru") {
setSortByOpt("createdAt");
} else {
setSortByOpt("clickCount");
}
setChange(!change);
};
async function getUserLevels() {
const res = await getUserLevelListByParent(761);
const userLevelList = res?.data?.data;
if (userLevelList !== null) {
let optionArr: any = [];
userLevelList?.map((option: any) => {
let optionData = {
id: option.id,
label: option.name,
value: option.id,
};
optionArr.push(optionData);
});
setUserLevels(optionArr);
}
}
const handleKeyUp = () => {
clearTimeout(typingTimer);
typingTimer = setTimeout(doneTyping, doneTypingInterval);
};
async function doneTyping() {
if (searchTitle == "" || searchTitle == undefined) {
router.push("?title=");
} else {
router.push(`?title=${searchTitle}`);
}
}
const handleKeyDown = () => {
clearTimeout(typingTimer);
};
return (
<div className="flex flex-col w-full max-w-screen overflow-x-hidden">
<div className="flex flex-row md:flex-row items-start gap-3 py-10 px-4 lg:px-20 bg-[#f7f7f7] dark:bg-black">
<p> {t("content", { defaultValue: "Content" })}</p>
{">"}
<p>
<span className="font-bold">
{t("allContent", { defaultValue: "All Content" })}
</span>
</p>
<p className="font-bold">|</p>
<p>{`${t("thereIs", { defaultValue: "There Is" })} ${totalContent} ${t(
"downloadableContent",
{ defaultValue: "Downloadable ContentS" }
)}`}</p>
</div>
{/* Left */}
<div className="flex flex-col lg:flex-row gap-6 pl-4 lg:pl-20 py-4">
<div className="lg:hidden flex justify-end mb-2">
<button
onClick={() => setIsFilterOpen(!isFilterOpen)}
className="text-sm text-white bg-[#bb3523] px-4 py-1 rounded-md shadow"
>
{isFilterOpen ? "Hide Filter" : "Show Filter"}
</button>
</div>
{isFilterOpen && (
<div className="h-fit min-w-full lg:min-w-[280px] max-w-full lg:max-w-[300px] bg-[#f7f7f7] dark:bg-black p-4 rounded-lg shadow-md">
<h2 className="text-lg font-semibold mb-4 flex items-center gap-1">
<Icon icon="stash:filter-light" fontSize={30} />
Filter
</h2>
<div className="border-t border-black my-4 dark:border-white"></div>
<div className="space-y-6">
<div>
<label
htmlFor="search"
className="block text-sm font-medium text-gray-700 dark:text-white"
>
{t("search", { defaultValue: "Search" })}
</label>
<Input
value={searchTitle}
onChange={(e) => setSearchTitle(e.target.value)}
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
type="text"
id="search"
placeholder={t("searchTitle", {
defaultValue: "Search Title",
})}
className="mt-1 w-full border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-white">
{t("monthYear", { defaultValue: "Month Year" })}
</label>
<ReactDatePicker
selected={monthYearFilter}
className="mt-1 w-full text-xs border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500"
onChange={(date) => setMonthYearFilter(date)}
dateFormat="MM | yyyy"
placeholderText={t("selectYear", {
defaultValue: "Select Year",
})}
showMonthYearPicker
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-white">
{t("date", { defaultValue: "Date" })}
</label>
<div className="flex flex-row justify justify-between gap-2">
<ReactDatePicker
selectsRange
className="mt-1 w-full border text-sm rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500"
startDate={dateRange[0]}
endDate={dateRange[1]}
onChange={(update) => {
setDateRange(update);
}}
placeholderText={t("selectDate", {
defaultValue: "Select Date",
})}
onCalendarClose={() => setCalenderState(!calenderState)}
/>
<div className="flex items-center">
{handleClose ? (
<Icon
icon="carbon:close-filled"
onClick={handleDeleteDate}
width="20"
inline
color="#216ba5"
/>
) : (
""
)}
</div>
</div>
</div>
<div>
<h3 className="text-sm font-medium text-gray-700 dark:text-white">
{t("categories", { defaultValue: "Categories" })}
</h3>
<ul className="mt-2 space-y-2">
{categories.map((category: any) => (
<li key={category?.id}>
<label
className="inline-flex items-center"
htmlFor={`${category.id}`}
>
<Checkbox
id={`${category.id}`}
value={category.id}
checked={categoryFilter.includes(String(category.id))}
onCheckedChange={(e) =>
handleCategoryFilter(Boolean(e), category.id)
}
/>
<span className="ml-2 text-gray-700 dark:text-white">
{category?.name}
</span>
</label>
</li>
))}
<div className="mt-4 flex gap-2 justify-center items-center">
<button
onClick={() =>
setCategoryPage((prev) => Math.max(prev - 1, 1))
}
disabled={categoryPage === 1}
className="px-3 py-1 border rounded disabled:opacity-50"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="m13.15 16.15l-3.625-3.625q-.125-.125-.175-.25T9.3 12t.05-.275t.175-.25L13.15 7.85q.075-.075.163-.112T13.5 7.7q.2 0 .35.138T14 8.2v7.6q0 .225-.15.363t-.35.137q-.05 0-.35-.15"
/>
</svg>
</button>
{Array.from({ length: categoryTotalPages }, (_, i) => (
<button
key={i}
onClick={() => setCategoryPage(i + 1)}
className={`px-3 py-1 border rounded ${
categoryPage === i + 1
? "bg-[#bb3523] text-white"
: ""
}`}
>
{i + 1}
</button>
))}
<button
onClick={() =>
setCategoryPage((prev) =>
Math.min(prev + 1, categoryTotalPages)
)
}
disabled={categoryPage === categoryTotalPages}
className="px-3 py-1 border rounded disabled:opacity-50"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M10.5 16.3q-.2 0-.35-.137T10 15.8V8.2q0-.225.15-.362t.35-.138q.05 0 .35.15l3.625 3.625q.125.125.175.25t.05.275t-.05.275t-.175.25L10.85 16.15q-.075.075-.162.113t-.188.037"
/>
</svg>
</button>
</div>
</ul>
</div>
{/* Garis */}
<div className="border-t border-black my-4 dark:border-white"></div>
{/* Garis */}
<div>
<h3 className="text-sm font-medium text-gray-700 dark:text-white">
Format
</h3>
<ul className="mt-2 space-y-2">
<li>
<label className="inline-flex items-center">
<Checkbox
id="png"
value="png"
checked={formatFilter.includes("png")}
onCheckedChange={(e) =>
handleFormatFilter(Boolean(e), "png")
}
/>
<span className="ml-2 text-gray-700 dark:text-white">
PNG
</span>
</label>
</li>
<li>
<label className="inline-flex items-center">
<Checkbox
id="jpeg"
value="jpeg"
checked={formatFilter.includes("jpeg")}
onCheckedChange={(e) =>
handleFormatFilter(Boolean(e), "jpeg")
}
/>
<span className="ml-2 text-gray-700 dark:text-white">
JPEG
</span>
</label>
</li>
<li>
<label className="inline-flex items-center">
<Checkbox
id="jpg"
value="jpg"
checked={formatFilter.includes("jpg")}
onCheckedChange={(e) =>
handleFormatFilter(Boolean(e), "jpg")
}
/>
<span className="ml-2 text-gray-700 dark:text-white">
JPG
</span>
</label>
</li>
</ul>
</div>
<div className="border-t border-black dark:border-white my-4"></div>
<div className="text-center">
<a
onClick={cleanCheckbox}
className="text-[#bb3523] cursor-pointer"
>
<b>Reset Filter</b>
</a>
</div>
</div>
</div>
)}
{/* Konten Kanan */}
<div className="w-full pl-4 pr-4 lg:pr-16 pb-4 overflow-x-hidden">
<div className="w-full">
<div className="flex flex-col items-end mb-4">
<h2 className="text-lg font-semibold">
{t("sortBy", { defaultValue: "Sort By" })}
</h2>
<select
defaultValue={sortBy == "popular" ? "terpopuler" : "terbaru"}
onChange={(e) => handleSorting(e)}
className="border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500"
>
<option value="latest">
{t("latest", { defaultValue: "Latest" })}
</option>
<option value="popular">
{t("mostPopular", { defaultValue: "Most Popular" })}
</option>
</select>
</div>
<div className="flex flex-col gap-4 w-full">
<FilterImageComponent
categoryFilter={categoryFilter}
sortByOpt={sortByOpt}
startDateString={startDateString}
endDateString={endDateString}
monthYearFilter={monthYearFilter}
/>
<FilterVideoComponent
categoryFilter={categoryFilter}
sortByOpt={sortByOpt}
startDateString={startDateString}
endDateString={endDateString}
monthYearFilter={monthYearFilter}
/>
<FilterDocumentComponent
categoryFilter={categoryFilter}
sortByOpt={sortByOpt}
startDateString={startDateString}
endDateString={endDateString}
monthYearFilter={monthYearFilter}
/>
<FilterAudioComponent
categoryFilter={categoryFilter}
sortByOpt={sortByOpt}
startDateString={startDateString}
endDateString={endDateString}
monthYearFilter={monthYearFilter}
/>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,14 @@
import Footer from "@/components/landing-page/footer";
import Navbar from "@/components/landing-page/navbar";
const layout = async ({ children }: { children: React.ReactNode }) => {
return (
<>
<Navbar />
{children}
<Footer />
</>
);
};
export default layout;

View File

@ -295,7 +295,6 @@ const Schedule = (props: any) => {
}; };
temp.push(now); temp.push(now);
} }
console.log("QQQQQ", temp);
setCity(temp); setCity(temp);
} }
fetchPoldaPolres(); fetchPoldaPolres();
@ -341,7 +340,6 @@ const Schedule = (props: any) => {
curr.setDate(curr.getDate() + 1); curr.setDate(curr.getDate() + 1);
} }
console.log("Change Date :", dateListTemp);
setDateList(dateListTemp); setDateList(dateListTemp);
setDateAWeek(dateListTemp); setDateAWeek(dateListTemp);
} }
@ -431,7 +429,6 @@ const Schedule = (props: any) => {
const response = await detailSchedule(itemFound?.id); const response = await detailSchedule(itemFound?.id);
setDetail(response?.data?.data); setDetail(response?.data?.data);
setContent(response?.data?.data?.files); setContent(response?.data?.data?.files);
console.log("item Found", itemFound);
close(); close();
setOpenDialog(true); setOpenDialog(true);
}; };
@ -483,7 +480,6 @@ const Schedule = (props: any) => {
}; };
const deleteFilterhandler = (filterId: any) => { const deleteFilterhandler = (filterId: any) => {
console.log("hapus", filterId);
const deletedReg = regionName.filter((list: any) => list.id !== filterId); const deletedReg = regionName.filter((list: any) => list.id !== filterId);
const filtered = regionFilter.filter((list: any) => list !== filterId); const filtered = regionFilter.filter((list: any) => list !== filterId);

View File

@ -505,10 +505,16 @@ const FilterPage = () => {
<p> <p>
{" "} {" "}
{t("video", { defaultValue: "Video" })}&nbsp; {t("video", { defaultValue: "Video" })}&nbsp;
{">"} <span className="font-bold">{t("allVideo", { defaultValue: "All Video" })}</span> {">"}{" "}
<span className="font-bold">
{t("allVideo", { defaultValue: "All Video" })}
</span>
</p> </p>
<p className="font-bold">|</p> <p className="font-bold">|</p>
<p>{`${t("thereIs", { defaultValue: "There Is" })} ${totalContent} ${t("downloadableVideo", { defaultValue: "Downloadable Video" })}`}</p> <p>{`${t("thereIs", { defaultValue: "There Is" })} ${totalContent} ${t(
"downloadableVideo",
{ defaultValue: "Downloadable Video" }
)}`}</p>
</div> </div>
{/* Left */} {/* Left */}
@ -548,7 +554,9 @@ const FilterPage = () => {
className="mt-1 w-full text-xs border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500" className="mt-1 w-full text-xs border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500"
onChange={(date) => setMonthYearFilter(date)} onChange={(date) => setMonthYearFilter(date)}
dateFormat="MM | yyyy" dateFormat="MM | yyyy"
placeholderText={t("selectYear", { defaultValue: "Select Year" })} placeholderText={t("selectYear", {
defaultValue: "Select Year",
})}
showMonthYearPicker showMonthYearPicker
/> />
</div> </div>
@ -566,7 +574,9 @@ const FilterPage = () => {
onChange={(update) => { onChange={(update) => {
setDateRange(update); setDateRange(update);
}} }}
placeholderText={t("selectDate", { defaultValue: "Select Date" })} placeholderText={t("selectDate", {
defaultValue: "Select Date",
})}
onCalendarClose={() => setCalenderState(!calenderState)} onCalendarClose={() => setCalenderState(!calenderState)}
/> />
<div className="flex items-center"> <div className="flex items-center">
@ -611,7 +621,6 @@ const FilterPage = () => {
</li> </li>
))} ))}
<div className="mt-4 flex gap-2 justify-center items-center"> <div className="mt-4 flex gap-2 justify-center items-center">
{/* Tombol Prev */}
<button <button
onClick={() => onClick={() =>
setCategoryPage((prev) => Math.max(prev - 1, 1)) setCategoryPage((prev) => Math.max(prev - 1, 1))
@ -632,7 +641,6 @@ const FilterPage = () => {
</svg> </svg>
</button> </button>
{/* Nomor Halaman */}
{Array.from({ length: categoryTotalPages }, (_, i) => ( {Array.from({ length: categoryTotalPages }, (_, i) => (
<button <button
key={i} key={i}
@ -645,7 +653,6 @@ const FilterPage = () => {
</button> </button>
))} ))}
{/* Tombol Next */}
<button <button
onClick={() => onClick={() =>
setCategoryPage((prev) => setCategoryPage((prev) =>
@ -772,14 +779,20 @@ const FilterPage = () => {
<Reveal> <Reveal>
<div className="w-full"> <div className="w-full">
<div className="flex flex-col items-end mb-4"> <div className="flex flex-col items-end mb-4">
<h2 className="text-lg font-semibold">{t("sortBy", { defaultValue: "Sort By" })}</h2> <h2 className="text-lg font-semibold">
{t("sortBy", { defaultValue: "Sort By" })}
</h2>
<select <select
defaultValue={sortBy == "popular" ? "terpopuler" : "terbaru"} defaultValue={sortBy == "popular" ? "terpopuler" : "terbaru"}
onChange={(e) => handleSorting(e)} onChange={(e) => handleSorting(e)}
className="border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500" className="border rounded-md py-2 px-3 focus:ring-red-500 focus:border-red-500"
> >
<option value="latest">{t("latest", { defaultValue: "Latest" })}</option> <option value="latest">
<option value="popular">{t("mostPopular", { defaultValue: "Most Popular" })}</option> {t("latest", { defaultValue: "Latest" })}
</option>
<option value="popular">
{t("mostPopular", { defaultValue: "Most Popular" })}
</option>
</select> </select>
</div> </div>

View File

@ -13,7 +13,8 @@ type AuthStep = "login" | "email-setup" | "otp";
const AuthPage = ({ params: { locale } }: { params: { locale: string } }) => { const AuthPage = ({ params: { locale } }: { params: { locale: string } }) => {
const [currentStep, setCurrentStep] = useState<AuthStep>("login"); const [currentStep, setCurrentStep] = useState<AuthStep>("login");
const [loginCredentials, setLoginCredentials] = useState<LoginFormData | null>(null); const [loginCredentials, setLoginCredentials] =
useState<LoginFormData | null>(null);
const { validateEmail } = useEmailValidation(); const { validateEmail } = useEmailValidation();
const { login } = useAuth(); const { login } = useAuth();
@ -23,6 +24,9 @@ const AuthPage = ({ params: { locale } }: { params: { locale: string } }) => {
try { try {
const result = await validateEmail(data); const result = await validateEmail(data);
switch (result) { switch (result) {
case "skip":
handleOTPSuccess();
break;
case "setup": case "setup":
setCurrentStep("email-setup"); setCurrentStep("email-setup");
break; break;
@ -112,11 +116,7 @@ const AuthPage = ({ params: { locale } }: { params: { locale: string } }) => {
} }
}; };
return ( return <AuthLayout>{renderCurrentStep()}</AuthLayout>;
<AuthLayout>
{renderCurrentStep()}
</AuthLayout>
);
}; };
export default AuthPage; export default AuthPage;

View File

@ -42,7 +42,7 @@ import {
Eye, Eye,
Settings, Settings,
CheckCircle, CheckCircle,
XCircle XCircle,
} from "lucide-react"; } from "lucide-react";
// Swiper // Swiper
@ -62,10 +62,7 @@ import {
getTagsBySubCategoryId, getTagsBySubCategoryId,
listEnableCategory, listEnableCategory,
} from "@/service/content/content"; } from "@/service/content/content";
import { import { generateDataRewrite, getDetailArticle } from "@/service/content/ai";
generateDataRewrite,
getDetailArticle,
} from "@/service/content/ai";
// Utils // Utils
import { getCookiesDecrypt } from "@/lib/utils"; import { getCookiesDecrypt } from "@/lib/utils";
@ -157,7 +154,7 @@ const CustomEditor = dynamic(
<Loader2 className="h-6 w-6 animate-spin" /> <Loader2 className="h-6 w-6 animate-spin" />
<span className="ml-2">Loading editor...</span> <span className="ml-2">Loading editor...</span>
</div> </div>
) ),
} }
); );
@ -190,9 +187,14 @@ export default function FormConvertSPIT() {
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
const [detail, setDetail] = useState<Detail | null>(null); const [detail, setDetail] = useState<Detail | null>(null);
const [categories, setCategories] = useState<Category[]>([]); const [categories, setCategories] = useState<Category[]>([]);
const [selectedCategoryId, setSelectedCategoryId] = useState<number | null>(null); const [selectedCategoryId, setSelectedCategoryId] = useState<number | null>(
const [selectedFileType, setSelectedFileType] = useState<"original" | "rewrite">("original"); null
const [selectedWritingStyle, setSelectedWritingStyle] = useState("professional"); );
const [selectedFileType, setSelectedFileType] = useState<
"original" | "rewrite"
>("original");
const [selectedWritingStyle, setSelectedWritingStyle] =
useState("professional");
const [showRewriteEditor, setShowRewriteEditor] = useState(false); const [showRewriteEditor, setShowRewriteEditor] = useState(false);
const [isGeneratingRewrite, setIsGeneratingRewrite] = useState(false); const [isGeneratingRewrite, setIsGeneratingRewrite] = useState(false);
const [isLoadingRewrite, setIsLoadingRewrite] = useState(false); const [isLoadingRewrite, setIsLoadingRewrite] = useState(false);
@ -205,7 +207,9 @@ export default function FormConvertSPIT() {
// Content rewrite state // Content rewrite state
const [articleIds, setArticleIds] = useState<string[]>([]); const [articleIds, setArticleIds] = useState<string[]>([]);
const [selectedArticleId, setSelectedArticleId] = useState<string | null>(null); const [selectedArticleId, setSelectedArticleId] = useState<string | null>(
null
);
const [articleBody, setArticleBody] = useState<string>(""); const [articleBody, setArticleBody] = useState<string>("");
// Form data state // Form data state
@ -304,8 +308,8 @@ export default function FormConvertSPIT() {
setDetail(details); setDetail(details);
setFiles(details.contentList || []); setFiles(details.contentList || []);
setDetailThumb( setDetailThumb(
(details.contentList || []).map((file: FileType) => (details.contentList || []).map(
file.contentFile || "default-image.jpg" (file: FileType) => file.contentFile || "default-image.jpg"
) )
); );
@ -317,7 +321,10 @@ export default function FormConvertSPIT() {
setValue("contentTitle", details.contentTitle || ""); setValue("contentTitle", details.contentTitle || "");
setValue("contentDescription", details.contentDescription || ""); setValue("contentDescription", details.contentDescription || "");
setValue("contentCreator", details.contentCreator || ""); setValue("contentCreator", details.contentCreator || "");
setValue("contentRewriteDescription", details.contentRewriteDescription || ""); setValue(
"contentRewriteDescription",
details.contentRewriteDescription || ""
);
// Set category and load category tags // Set category and load category tags
if (details.categoryId) { if (details.categoryId) {
@ -374,7 +381,7 @@ export default function FormConvertSPIT() {
} }
const newArticleId = response?.data?.data?.id; const newArticleId = response?.data?.data?.id;
setArticleIds(prev => { setArticleIds((prev) => {
const updated = [...prev]; const updated = [...prev];
if (updated.length < 3) { if (updated.length < 3) {
updated.push(newArticleId); updated.push(newArticleId);
@ -417,14 +424,17 @@ export default function FormConvertSPIT() {
const articleData = response?.data?.data; const articleData = response?.data?.data;
if (articleData?.status === 2) { if (articleData?.status === 2) {
const cleanArticleBody = articleData.articleBody?.replace(/<img[^>]*>/g, ""); const cleanArticleBody = articleData.articleBody?.replace(
/<img[^>]*>/g,
""
);
setArticleBody(cleanArticleBody || ""); setArticleBody(cleanArticleBody || "");
setValue("contentRewriteDescription", cleanArticleBody || ""); setValue("contentRewriteDescription", cleanArticleBody || "");
break; break;
} }
retryCount++; retryCount++;
await new Promise(resolve => setTimeout(resolve, 5000)); await new Promise((resolve) => setTimeout(resolve, 5000));
} }
if (retryCount >= maxRetries) { if (retryCount >= maxRetries) {
@ -449,7 +459,7 @@ export default function FormConvertSPIT() {
const newTag = inputRef.current.value.trim(); const newTag = inputRef.current.value.trim();
if (!tags.includes(newTag)) { if (!tags.includes(newTag)) {
setTags(prev => [...prev, newTag]); setTags((prev) => [...prev, newTag]);
} }
inputRef.current.value = ""; inputRef.current.value = "";
@ -457,27 +467,33 @@ export default function FormConvertSPIT() {
}; };
const handleRemoveTag = (index: number) => { const handleRemoveTag = (index: number) => {
setTags(prev => prev.filter((_, i) => i !== index)); setTags((prev) => prev.filter((_, i) => i !== index));
}; };
const handlePublishTargetChange = (optionId: string) => { const handlePublishTargetChange = (optionId: string) => {
if (optionId === "all") { if (optionId === "all") {
setPublishedFor(prev => setPublishedFor((prev) =>
prev.length === PUBLISH_OPTIONS.filter(opt => opt.id !== "all").length prev.length === PUBLISH_OPTIONS.filter((opt) => opt.id !== "all").length
? [] ? []
: PUBLISH_OPTIONS.filter(opt => opt.id !== "all").map(opt => opt.id) : PUBLISH_OPTIONS.filter((opt) => opt.id !== "all").map(
(opt) => opt.id
)
); );
} else { } else {
setPublishedFor(prev => setPublishedFor((prev) =>
prev.includes(optionId) prev.includes(optionId)
? prev.filter(id => id !== optionId && id !== "all") ? prev.filter((id) => id !== optionId && id !== "all")
: [...prev.filter(id => id !== "all"), optionId] : [...prev.filter((id) => id !== "all"), optionId]
); );
} }
}; };
const handleFilePlacementChange = (fileIndex: number, placement: string, checked: boolean) => { const handleFilePlacementChange = (
setFilePlacements(prev => { fileIndex: number,
placement: string,
checked: boolean
) => {
setFilePlacements((prev) => {
const updated = [...prev]; const updated = [...prev];
const currentPlacements = updated[fileIndex] || []; const currentPlacements = updated[fileIndex] || [];
@ -495,9 +511,11 @@ export default function FormConvertSPIT() {
if (placement === "all") { if (placement === "all") {
updated[fileIndex] = []; updated[fileIndex] = [];
} else { } else {
const newPlacements = currentPlacements.filter(p => p !== placement); const newPlacements = currentPlacements.filter(
(p) => p !== placement
);
if (newPlacements.length === 3 && newPlacements.includes("all")) { if (newPlacements.length === 3 && newPlacements.includes("all")) {
updated[fileIndex] = newPlacements.filter(p => p !== "all"); updated[fileIndex] = newPlacements.filter((p) => p !== "all");
} else { } else {
updated[fileIndex] = newPlacements; updated[fileIndex] = newPlacements;
} }
@ -509,11 +527,11 @@ export default function FormConvertSPIT() {
}; };
const handleSelectAllPlacements = (placement: string, checked: boolean) => { const handleSelectAllPlacements = (placement: string, checked: boolean) => {
setFilePlacements(prev => setFilePlacements((prev) =>
prev.map(filePlacements => prev.map((filePlacements) =>
checked checked
? Array.from(new Set([...filePlacements, placement])) ? Array.from(new Set([...filePlacements, placement]))
: filePlacements.filter(p => p !== placement) : filePlacements.filter((p) => p !== placement)
) )
); );
}; };
@ -533,14 +551,37 @@ export default function FormConvertSPIT() {
return placementData; return placementData;
}; };
const checkPlacement = (data: any) => {
let temp = true;
for (const element of data) {
if (element.length < 1) {
temp = false;
break;
}
}
return temp;
};
// Form submission // Form submission
const onSubmit = async (data: FormData) => { const onSubmit = async (data: FormData) => {
if (!checkPlacement(filePlacements)) {
error("Select File Placement");
return false;
}
if (publishedFor.length < 1) {
error("Select Publish Target");
return false;
}
try { try {
setIsSaving(true); setIsSaving(true);
const description = selectedFileType === "original" const description =
? data.contentDescription selectedFileType === "original"
: data.contentRewriteDescription; ? data.contentDescription
: data.contentRewriteDescription;
const requestData = { const requestData = {
spitId: id, spitId: id,
@ -633,17 +674,15 @@ export default function FormConvertSPIT() {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold tracking-tight">SPIT Convert Form</h1> <h1 className="text-3xl font-bold tracking-tight">
SPIT Convert Form
</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Convert and manage your SPIT content Convert and manage your SPIT content
</p> </p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button variant="outline" size="sm" onClick={() => router.back()}>
variant="outline"
size="sm"
onClick={() => router.back()}
>
<XCircle className="h-4 w-4 mr-2" /> <XCircle className="h-4 w-4 mr-2" />
Cancel Cancel
</Button> </Button>
@ -737,7 +776,9 @@ export default function FormConvertSPIT() {
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<RadioGroup <RadioGroup
value={selectedFileType} value={selectedFileType}
onValueChange={(value: "original" | "rewrite") => setSelectedFileType(value)} onValueChange={(value: "original" | "rewrite") =>
setSelectedFileType(value)
}
className="grid grid-cols-2 gap-4" className="grid grid-cols-2 gap-4"
> >
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
@ -792,7 +833,9 @@ export default function FormConvertSPIT() {
<Button <Button
type="button" type="button"
onClick={handleRewriteClick} onClick={handleRewriteClick}
disabled={isGeneratingRewrite || !detail?.contentDescription} disabled={
isGeneratingRewrite || !detail?.contentDescription
}
className="bg-blue-600 hover:bg-blue-700" className="bg-blue-600 hover:bg-blue-700"
> >
{isGeneratingRewrite ? ( {isGeneratingRewrite ? (
@ -812,14 +855,19 @@ export default function FormConvertSPIT() {
<Button <Button
key={articleId} key={articleId}
type="button" type="button"
variant={selectedArticleId === articleId ? "default" : "outline"} variant={
selectedArticleId === articleId
? "default"
: "outline"
}
size="sm" size="sm"
onClick={() => handleArticleSelect(articleId)} onClick={() => handleArticleSelect(articleId)}
disabled={isLoadingRewrite} disabled={isLoadingRewrite}
> >
{isLoadingRewrite && selectedArticleId === articleId && ( {isLoadingRewrite &&
<Loader2 className="h-4 w-4 mr-2 animate-spin" /> selectedArticleId === articleId && (
)} <Loader2 className="h-4 w-4 mr-2 animate-spin" />
)}
Narrative {index + 1} Narrative {index + 1}
</Button> </Button>
))} ))}
@ -909,14 +957,23 @@ export default function FormConvertSPIT() {
{files.length > 1 && ( {files.length > 1 && (
<div className="flex flex-wrap gap-4 p-4 bg-muted/50 rounded-lg"> <div className="flex flex-wrap gap-4 p-4 bg-muted/50 rounded-lg">
{PLACEMENT_OPTIONS.map((option) => ( {PLACEMENT_OPTIONS.map((option) => (
<div key={option.value} className="flex items-center space-x-2"> <div
key={option.value}
className="flex items-center space-x-2"
>
<Checkbox <Checkbox
id={`select-all-${option.value}`} id={`select-all-${option.value}`}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
handleSelectAllPlacements(option.value, Boolean(checked)) handleSelectAllPlacements(
option.value,
Boolean(checked)
)
} }
/> />
<Label htmlFor={`select-all-${option.value}`} className="text-sm"> <Label
htmlFor={`select-all-${option.value}`}
className="text-sm"
>
All {option.label} All {option.label}
</Label> </Label>
</div> </div>
@ -939,12 +996,21 @@ export default function FormConvertSPIT() {
<p className="font-medium text-sm">{file.fileName}</p> <p className="font-medium text-sm">{file.fileName}</p>
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
{PLACEMENT_OPTIONS.map((option) => ( {PLACEMENT_OPTIONS.map((option) => (
<div key={option.value} className="flex items-center space-x-2"> <div
key={option.value}
className="flex items-center space-x-2"
>
<Checkbox <Checkbox
id={`${file.contentId}-${option.value}`} id={`${file.contentId}-${option.value}`}
checked={filePlacements[index]?.includes(option.value)} checked={filePlacements[index]?.includes(
option.value
)}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
handleFilePlacementChange(index, option.value, Boolean(checked)) handleFilePlacementChange(
index,
option.value,
Boolean(checked)
)
} }
/> />
<Label <Label
@ -1070,10 +1136,14 @@ export default function FormConvertSPIT() {
id={option.id} id={option.id}
checked={ checked={
option.id === "all" option.id === "all"
? publishedFor.length === PUBLISH_OPTIONS.filter(opt => opt.id !== "all").length ? publishedFor.length ===
PUBLISH_OPTIONS.filter((opt) => opt.id !== "all")
.length
: publishedFor.includes(option.id) : publishedFor.includes(option.id)
} }
onCheckedChange={() => handlePublishTargetChange(option.id)} onCheckedChange={() =>
handlePublishTargetChange(option.id)
}
/> />
<Label htmlFor={option.id} className="text-sm"> <Label htmlFor={option.id} className="text-sm">
{option.label} {option.label}

View File

@ -606,7 +606,7 @@ export default function FormVideoUpdate() {
const fileList = files.map((file: any) => ( const fileList = files.map((file: any) => (
<div <div
key={file.id} // Gunakan ID file sebagai key key={file.id}
className="flex justify-between border px-3.5 py-3 my-6 rounded-md" className="flex justify-between border px-3.5 py-3 my-6 rounded-md"
> >
<div className="flex gap-3 items-center"> <div className="flex gap-3 items-center">
@ -995,7 +995,7 @@ export default function FormVideoUpdate() {
)} )}
</div> </div>
</div> </div>
<div className="mt-3 px-3 space-y-2"> {/* <div className="mt-3 px-3 space-y-2">
<Label>{t("preview", { defaultValue: "Preview" })}</Label> <Label>{t("preview", { defaultValue: "Preview" })}</Label>
<Card className="mt-2"> <Card className="mt-2">
<img <img
@ -1004,7 +1004,39 @@ export default function FormVideoUpdate() {
className="w-full h-auto rounded" className="w-full h-auto rounded"
/> />
</Card> </Card>
</div> */}
<div className="mt-3 px-3 space-y-2">
<Label>{t("preview", { defaultValue: "Preview" })}</Label>
<Input
type="file"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
setSelectedFiles([file]);
}
}}
className="dark:border dark:border-gray-500 dark:rounded-lg"
/>
{selectedFiles.length > 0 ? (
<Card className="mt-2">
<img
src={URL.createObjectURL(selectedFiles[0])}
alt="Thumbnail Baru"
className="w-full h-auto rounded"
/>
</Card>
) : (
<Card className="mt-2">
<img
src={detail?.thumbnailLink}
alt="Thumbnail Lama"
className="w-full h-auto rounded"
/>
</Card>
)}
</div> </div>
<div className="px-3 py-3"> <div className="px-3 py-3">
<div className="space-y-2"> <div className="space-y-2">
<Label>{t("tags", { defaultValue: "Tags" })}</Label> <Label>{t("tags", { defaultValue: "Tags" })}</Label>

View File

@ -15,6 +15,8 @@ import Coverage from "./coverage";
import Division from "./division"; import Division from "./division";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { useRouter } from "@/i18n/routing";
import { Icon } from "../ui/icon";
const AreaCoverageWorkUnits = () => { const AreaCoverageWorkUnits = () => {
const [openPolda, setOpenPolda] = useState(false); const [openPolda, setOpenPolda] = useState(false);
@ -22,6 +24,16 @@ const AreaCoverageWorkUnits = () => {
const t = useTranslations("LandingPage"); const t = useTranslations("LandingPage");
const params = useParams(); const params = useParams();
const locale = params?.locale; const locale = params?.locale;
const [search, setSearch] = useState("");
const router = useRouter();
const poldaName = params?.polda_name;
const satkerName = params?.satker_name;
let prefixPath = poldaName
? `/polda/${poldaName}`
: satkerName
? `/satker/${satkerName}`
: "";
useEffect(() => { useEffect(() => {
if (openPolda || openSatker) { if (openPolda || openSatker) {
@ -42,9 +54,28 @@ const AreaCoverageWorkUnits = () => {
<h2 className="text-start text-lg md:text-xl font-bold text-[#bb3523] border-b-2 border-[#bb3523] mb-4 uppercase"> <h2 className="text-start text-lg md:text-xl font-bold text-[#bb3523] border-b-2 border-[#bb3523] mb-4 uppercase">
{t("areaCoverage", { defaultValue: "Area Coverage" })} {t("areaCoverage", { defaultValue: "Area Coverage" })}
</h2> </h2>
{/* search */}
<div className="flex flex-row gap-2 mb-2">
<input
type="text"
placeholder={t("searchCoverageHere", {
defaultValue: "Search Coverage Here",
})}
className="w-full py-4 px-2 text-sm text-gray-700 dark:text-gray-100 focus:outline-none rounded-md"
onChange={(e) => setSearch(e.target.value)}
/>
<button
onClick={() =>
router.push(prefixPath + `/regional/filter?title=${search}`)
}
className="flex justify-center items-center px-6 w-full lg:w-[20%] py-4 bg-[#bb3523] gap-2 text-white rounded-lg hover:bg-red-700 text-[14px]"
>
{t("searchCoverage", { defaultValue: "Search Coverage" })}
<Icon icon="ri:arrow-right-s-line" fontSize={20} />
</button>
</div>
<div className="flex flex-col justify-center lg:flex-row gap-8 "> <div className="flex flex-col justify-center lg:flex-row gap-8 ">
{/* POLDA */} {/* POLDA */}
<Dialog open={openPolda} onOpenChange={setOpenPolda}> <Dialog open={openPolda} onOpenChange={setOpenPolda}>
<DialogTrigger asChild> <DialogTrigger asChild>
<button <button
@ -59,7 +90,9 @@ const AreaCoverageWorkUnits = () => {
src="/assets/indo.png" src="/assets/indo.png"
className="h-32 w-32 group-hover:scale-110 group-hover:border-[#bb3523] " className="h-32 w-32 group-hover:scale-110 group-hover:border-[#bb3523] "
/> />
<p className="text-base font-bold">{t("regionalPolice", { defaultValue: "Regional Police" })}</p> <p className="text-base font-bold">
{t("regionalPolice", { defaultValue: "Regional Police" })}
</p>
</button> </button>
</DialogTrigger> </DialogTrigger>
<DialogContent <DialogContent
@ -69,7 +102,9 @@ const AreaCoverageWorkUnits = () => {
> >
<DialogHeader className="flex flex-col justify-center"> <DialogHeader className="flex flex-col justify-center">
<DialogTitle> <DialogTitle>
<p className="text-center">{t("regionalPolice", { defaultValue: "Regional Police" })}</p> <p className="text-center">
{t("regionalPolice", { defaultValue: "Regional Police" })}
</p>
</DialogTitle> </DialogTitle>
<DialogTitle> <DialogTitle>
<div className="h-1 w-[150px] bg-[#bb3523] mx-auto mb-6 rounded"></div> <div className="h-1 w-[150px] bg-[#bb3523] mx-auto mb-6 rounded"></div>
@ -89,7 +124,6 @@ const AreaCoverageWorkUnits = () => {
</Dialog> </Dialog>
{/* SATKER */} {/* SATKER */}
<Dialog open={openSatker} onOpenChange={setOpenSatker}> <Dialog open={openSatker} onOpenChange={setOpenSatker}>
<DialogTrigger asChild> <DialogTrigger asChild>
<button className="flex flex-col gap-2 justify-center items-center shadow-lg group rounded-xl py-5 w-full border-2 border-transparent hover:border-[#bb3523] transition-all duration-300"> <button className="flex flex-col gap-2 justify-center items-center shadow-lg group rounded-xl py-5 w-full border-2 border-transparent hover:border-[#bb3523] transition-all duration-300">
@ -101,13 +135,17 @@ const AreaCoverageWorkUnits = () => {
src="/assets/logo-polri.png" src="/assets/logo-polri.png"
className="h-32 w-32 group-hover:scale-110 group-hover:border-[#bb3523] transition-transform duration-300" className="h-32 w-32 group-hover:scale-110 group-hover:border-[#bb3523] transition-transform duration-300"
/> />
<p className="text-base font-bold">{t("policeDivision", { defaultValue: "Police Division" })}</p> <p className="text-base font-bold">
{t("policeDivision", { defaultValue: "Police Division" })}
</p>
</button> </button>
</DialogTrigger> </DialogTrigger>
<DialogContent size="md" data-lenis-prevent> <DialogContent size="md" data-lenis-prevent>
<DialogHeader className="flex flex-col justify-center"> <DialogHeader className="flex flex-col justify-center">
<DialogTitle> <DialogTitle>
<p className="text-center">{t("policeDivision", { defaultValue: "Police Division" })}</p> <p className="text-center">
{t("policeDivision", { defaultValue: "Police Division" })}
</p>
</DialogTitle> </DialogTitle>
<DialogTitle> <DialogTitle>
<div className="h-1 w-[150px] bg-[#bb3523] mx-auto mb-6 rounded"></div> <div className="h-1 w-[150px] bg-[#bb3523] mx-auto mb-6 rounded"></div>

View File

@ -8,7 +8,7 @@ import {
} from "@/components/ui/carousel"; } from "@/components/ui/carousel";
import { close, loading } from "@/config/swal"; import { close, loading } from "@/config/swal";
import { Link, usePathname } from "@/i18n/routing"; import { Link, usePathname } from "@/i18n/routing";
import { listData } from "@/service/landing/landing"; import { listData, listDataRegional } from "@/service/landing/landing";
import { formatDateToIndonesian, getOnlyMonthAndYear } from "@/utils/globals"; import { formatDateToIndonesian, getOnlyMonthAndYear } from "@/utils/globals";
import { Icon } from "@iconify/react/dist/iconify.js"; import { Icon } from "@iconify/react/dist/iconify.js";
import { pages } from "next/dist/build/templates/app-page"; import { pages } from "next/dist/build/templates/app-page";
@ -42,6 +42,50 @@ export default function FilterAudioComponent(props: {
const categorie = searchParams?.get("category"); const categorie = searchParams?.get("category");
const group = searchParams?.get("group"); const group = searchParams?.get("group");
const [totalContent, setTotalContent] = useState(); const [totalContent, setTotalContent] = useState();
const isRegional = group || asPath.includes("/polda/");
const searchType = isRegional ? "regional" : "filter";
useEffect(() => {
if (searchType === "regional") {
getDataRegional();
} else {
getDataAll();
}
}, [
title,
categoryFilter,
categorie,
group,
startDateString,
endDateString,
monthYearFilter,
sortByOpt,
]);
async function getDataRegional() {
let startDateFilter = startDateString ? startDateString : "";
let endDateFilter = endDateString ? endDateString : "";
let monthFilter = monthYearFilter ? monthYearFilter : "";
const response = await listDataRegional(
"4",
"",
"",
"",
"",
startDateFilter,
endDateFilter,
monthFilter
? getOnlyMonthAndYear(monthFilter)?.split("/")[0]?.replace("0", "")
: "",
monthFilter ? getOnlyMonthAndYear(monthFilter)?.split("/")[1] : ""
);
const data = response?.data?.data;
const contentData = data?.content;
setNewContent(contentData);
setTotalData(data?.totalElements);
// setLoading(false);
}
useEffect(() => { useEffect(() => {
getDataAll(); getDataAll();
@ -137,7 +181,7 @@ export default function FilterAudioComponent(props: {
return newContent?.length > 0 ? ( return newContent?.length > 0 ? (
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<p>{`Audio(${totalContent})`}</p> <p>{`Audio (${totalContent})`}</p>
<Carousel className="w-full max-w-7xl mx-auto"> <Carousel className="w-full max-w-7xl mx-auto">
<CarouselContent> <CarouselContent>
{newContent?.map((audio: any) => ( {newContent?.map((audio: any) => (
@ -145,7 +189,7 @@ export default function FilterAudioComponent(props: {
<div className="flex flex-row gap-6"> <div className="flex flex-row gap-6">
<Link <Link
href={`/audio/detail/${audio?.slug}`} href={`/audio/detail/${audio?.slug}`}
className="flex flex-col sm:flex-row items-center bg-white dark:bg-gray-800 cursor-pointer shadow-md rounded-lg p-4 gap-4 w-full" className="flex flex-col sm:flex-row items-center bg-white dark:bg-gray-800 cursor-pointer shadow- rounded-lg p-2 gap-4 w-full"
> >
<div className="flex items-center justify-center bg-red-500 text-white rounded-lg w-16 h-8 lg:h-16"> <div className="flex items-center justify-center bg-red-500 text-white rounded-lg w-16 h-8 lg:h-16">
<svg <svg
@ -163,11 +207,11 @@ export default function FilterAudioComponent(props: {
</div> </div>
<div className="flex flex-col flex-1"> <div className="flex flex-col flex-1">
<div className="text-gray-500 dark:text-gray-400 flex flex-row text-sm"> <div className="text-gray-500 dark:text-gray-400 flex flex-row text-sm items-center">
{formatDateToIndonesian(new Date(audio?.createdAt))}{" "} {formatDateToIndonesian(new Date(audio?.createdAt))}{" "}
{audio?.timezone ? audio?.timezone : "WIB"} |{" "} {audio?.timezone ? audio?.timezone : "WIB"} | &nbsp;
<Icon icon="formkit:eye" width="15" height="15" />{" "} <Icon icon="formkit:eye" width="15" height="15" />
{audio?.clickCount}{" "} &nbsp;{audio?.clickCount}{" "}
</div> </div>
<div className="font-semibold text-gray-900 dark:text-white mt-1 text-sm h-5 hover:h-auto truncate hover:whitespace-normal hover:overflow-visible"> <div className="font-semibold text-gray-900 dark:text-white mt-1 text-sm h-5 hover:h-auto truncate hover:whitespace-normal hover:overflow-visible">
{audio?.title} {audio?.title}
@ -178,9 +222,27 @@ export default function FilterAudioComponent(props: {
</CarouselItem> </CarouselItem>
))} ))}
</CarouselContent> </CarouselContent>
<CarouselPrevious /> <CarouselPrevious className="-ml-0" />
<CarouselNext /> <CarouselNext className="-mr-0" />
</Carousel> </Carousel>
<div className="flex justify-center mt-1 mb-6">
<Link
href={
`${
asPath.includes("/polda/")
? `/${asPath.split("/")[1]}/${asPath.split("/")[2]}`
: ""
}/audio/` +
`${group ? "regional?" : "filter?"}` +
`sortBy=${sortBy === "popular" ? "popular" : "latest"}` +
`${title ? `&title=${title.toLowerCase()}` : ""}` +
`${categorie ? `&category=${categorie}` : ""}`
}
className="border border-red-500 text-red-500 hover:bg-red-500 hover:text-white transition-colors duration-200 px-4 py-2 rounded-md text-sm"
>
Lihat Semua
</Link>
</div>
</div> </div>
) : ( ) : (
"" ""

View File

@ -8,7 +8,7 @@ import {
} from "@/components/ui/carousel"; } from "@/components/ui/carousel";
import { close, loading } from "@/config/swal"; import { close, loading } from "@/config/swal";
import { Link, usePathname } from "@/i18n/routing"; import { Link, usePathname } from "@/i18n/routing";
import { listData } from "@/service/landing/landing"; import { listData, listDataRegional } from "@/service/landing/landing";
import { formatDateToIndonesian, getOnlyMonthAndYear } from "@/utils/globals"; import { formatDateToIndonesian, getOnlyMonthAndYear } from "@/utils/globals";
import { Icon } from "@iconify/react/dist/iconify.js"; import { Icon } from "@iconify/react/dist/iconify.js";
import { pages } from "next/dist/build/templates/app-page"; import { pages } from "next/dist/build/templates/app-page";
@ -43,8 +43,15 @@ export default function FilterDocumentComponent(props: {
const group = searchParams?.get("group"); const group = searchParams?.get("group");
const [totalContent, setTotalContent] = useState(); const [totalContent, setTotalContent] = useState();
const isRegional = group || asPath.includes("/polda/");
const searchType = isRegional ? "regional" : "filter";
useEffect(() => { useEffect(() => {
getDataAll(); if (searchType === "regional") {
getDataRegional();
} else {
getDataAll();
}
}, [ }, [
title, title,
categoryFilter, categoryFilter,
@ -56,6 +63,31 @@ export default function FilterDocumentComponent(props: {
sortByOpt, sortByOpt,
]); ]);
async function getDataRegional() {
let startDateFilter = startDateString ? startDateString : "";
let endDateFilter = endDateString ? endDateString : "";
let monthFilter = monthYearFilter ? monthYearFilter : "";
const response = await listDataRegional(
"3",
"",
"",
"",
"",
startDateFilter,
endDateFilter,
monthFilter
? getOnlyMonthAndYear(monthFilter)?.split("/")[0]?.replace("0", "")
: "",
monthFilter ? getOnlyMonthAndYear(monthFilter)?.split("/")[1] : ""
);
const data = response?.data?.data;
const contentData = data?.content;
setNewContent(contentData);
setTotalData(data?.totalElements);
// setLoading(false);
}
async function getDataAll() { async function getDataAll() {
if (asPath?.includes("/polda/") == true) { if (asPath?.includes("/polda/") == true) {
if (asPath?.split("/")[2] !== "[polda_name]") { if (asPath?.split("/")[2] !== "[polda_name]") {
@ -137,7 +169,7 @@ export default function FilterDocumentComponent(props: {
return newContent?.length > 0 ? ( return newContent?.length > 0 ? (
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<p>{`Document(${totalContent})`}</p> <p>{`Document (${totalContent})`}</p>
<Carousel className="w-full max-w-7xl mx-auto"> <Carousel className="w-full max-w-7xl mx-auto">
<CarouselContent> <CarouselContent>
{newContent?.map((text: any) => ( {newContent?.map((text: any) => (
@ -164,12 +196,12 @@ export default function FilterDocumentComponent(props: {
<div className="flex w-full pr-10 flex-col flex-1"> <div className="flex w-full pr-10 flex-col flex-1">
<div className="text-gray-500 dark:text-gray-400 flex flex-row items-center text-xs gap-0 lg:gap-1 mt-1 lg:text-sm"> <div className="text-gray-500 dark:text-gray-400 flex flex-row items-center text-xs gap-0 lg:gap-1 mt-1 lg:text-sm">
{formatDateToIndonesian(new Date(text?.createdAt))} {formatDateToIndonesian(new Date(text?.createdAt))}&nbsp;
{text?.timezone ? text?.timezone : "WIB"}| {text?.timezone ? text?.timezone : "WIB"}&nbsp;|
<Icon icon="formkit:eye" width="15" height="15" /> <Icon icon="formkit:eye" width="15" height="15" />
{text?.clickCount} {text?.clickCount}
</div> </div>
<div className="font-semibold text-gray-900 dark:text-white mt-1 text-sm h-5 hover:h-auto truncate hover:whitespace-normal hover:overflow-visible "> <div className="font-semibold tet-gray-900 dark:text-white mt-1 text-sm h-5 hover:h-auto truncate hover:whitespace-normal hover:overflow-visible ">
{text?.title} {text?.title}
</div> </div>
<div className="flex gap-2 items-center text-sm text-red-500 dark:text-red-500"> <div className="flex gap-2 items-center text-sm text-red-500 dark:text-red-500">
@ -192,9 +224,25 @@ export default function FilterDocumentComponent(props: {
</CarouselItem> </CarouselItem>
))} ))}
</CarouselContent> </CarouselContent>
<CarouselPrevious /> <CarouselPrevious className="-ml-0" />
<CarouselNext /> <CarouselNext className="-mr-0" />
</Carousel> </Carousel>
<div className="flex justify-center mt-1 mb-6">
<Link
href={`${
asPath.includes("/polda/")
? `/${asPath.split("/")[1]}/${asPath.split("/")[2]}`
: ""
}/document/${group ? "filter" : "regional"}?sortBy=${
sortBy || "latest"
}${title ? `&title=${title.toLowerCase()}` : ""}${
categorie ? `&category=${categorie}` : ""
}${group ? `&group=${group}` : ""}`}
className="border border-red-500 text-red-500 hover:bg-red-500 hover:text-white transition-colors duration-200 px-4 py-2 rounded-md text-sm"
>
Lihat Semua
</Link>
</div>
</div> </div>
) : ( ) : (
"" ""

View File

@ -8,7 +8,7 @@ import {
} from "@/components/ui/carousel"; } from "@/components/ui/carousel";
import { close, loading } from "@/config/swal"; import { close, loading } from "@/config/swal";
import { Link, usePathname } from "@/i18n/routing"; import { Link, usePathname } from "@/i18n/routing";
import { listData } from "@/service/landing/landing"; import { listData, listDataRegional } from "@/service/landing/landing";
import { formatDateToIndonesian, getOnlyMonthAndYear } from "@/utils/globals"; import { formatDateToIndonesian, getOnlyMonthAndYear } from "@/utils/globals";
import { Icon } from "@iconify/react/dist/iconify.js"; import { Icon } from "@iconify/react/dist/iconify.js";
import { pages } from "next/dist/build/templates/app-page"; import { pages } from "next/dist/build/templates/app-page";
@ -42,8 +42,15 @@ export default function FilterImageComponent(props: {
const group = searchParams?.get("group"); const group = searchParams?.get("group");
const [totalContent, setTotalContent] = useState(); const [totalContent, setTotalContent] = useState();
const isRegional = group || asPath.includes("/polda/");
const searchType = isRegional ? "regional" : "filter";
useEffect(() => { useEffect(() => {
getDataAll(); if (searchType === "regional") {
getDataRegional();
} else {
getDataAll();
}
}, [ }, [
title, title,
categoryFilter, categoryFilter,
@ -134,10 +141,48 @@ export default function FilterImageComponent(props: {
} }
} }
async function getDataRegional() {
let startDateFilter = startDateString ? startDateString : "";
let endDateFilter = endDateString ? endDateString : "";
let monthFilter = monthYearFilter ? monthYearFilter : "";
const response = await listDataRegional(
"1",
"",
"",
"",
"",
startDateFilter,
endDateFilter,
monthFilter
? getOnlyMonthAndYear(monthFilter)?.split("/")[0]?.replace("0", "")
: "",
monthFilter ? getOnlyMonthAndYear(monthFilter)?.split("/")[1] : ""
);
const data = response?.data?.data;
const contentData = data?.content;
setNewContent(contentData);
setTotalData(data?.totalElements);
// setLoading(false);
}
const basePath = asPath.includes("/polda/")
? `/${asPath.split("/")[1]}/${asPath.split("/")[2]}`
: "";
const type = group || asPath.includes("/polda/") ? "regional?" : "filter?";
const sort = sortByOpt === "popular" ? "sortBy=popular" : "sortBy=latest";
const titleParam = title ? `&title=${title?.toLowerCase()}` : "";
const categoryParam = categorie ? `&category=${categorie}` : "";
const fullQuery = `${type}${sort}${titleParam}${categoryParam}${
startDateString ? `&startDate=${startDateString}` : ""
}${endDateString ? `&endDate=${endDateString}` : ""}`;
const href = `${basePath}/image/${fullQuery}`;
return newContent?.length > 0 ? ( return newContent?.length > 0 ? (
<div className="flex flex-col gap-3 w-full"> <div className="flex flex-col gap-3 w-full">
<p>{`Foto(${totalContent})`}</p> <p>{`Foto (${totalContent})`}</p>
<Carousel className="w-full max-w-7xl mx-auto"> <Carousel className="w-full max-w-7xl mx-auto mt-3">
<CarouselContent> <CarouselContent>
{newContent?.map((image: any) => ( {newContent?.map((image: any) => (
<CarouselItem key={image?.id} className="md:basis-1/2 lg:basis-1/3"> <CarouselItem key={image?.id} className="md:basis-1/2 lg:basis-1/3">
@ -164,9 +209,17 @@ export default function FilterImageComponent(props: {
</CarouselItem> </CarouselItem>
))} ))}
</CarouselContent> </CarouselContent>
<CarouselPrevious /> <CarouselPrevious className="-ml-0" />
<CarouselNext /> <CarouselNext className="-mr-0" />
</Carousel> </Carousel>
<div className="flex justify-center mt-1 mb-6">
<Link
href={href}
className="border border-red-500 text-red-500 hover:bg-red-500 text-sm hover:text-white px-4 py-2 rounded transition duration-200"
>
Lihat Semua
</Link>
</div>
</div> </div>
) : ( ) : (
"" ""

View File

@ -8,7 +8,7 @@ import {
} from "@/components/ui/carousel"; } from "@/components/ui/carousel";
import { close, loading } from "@/config/swal"; import { close, loading } from "@/config/swal";
import { Link, usePathname } from "@/i18n/routing"; import { Link, usePathname } from "@/i18n/routing";
import { listData } from "@/service/landing/landing"; import { listData, listDataRegional } from "@/service/landing/landing";
import { formatDateToIndonesian, getOnlyMonthAndYear } from "@/utils/globals"; import { formatDateToIndonesian, getOnlyMonthAndYear } from "@/utils/globals";
import { Icon } from "@iconify/react/dist/iconify.js"; import { Icon } from "@iconify/react/dist/iconify.js";
import { pages } from "next/dist/build/templates/app-page"; import { pages } from "next/dist/build/templates/app-page";
@ -43,8 +43,15 @@ export default function FilterVideoComponent(props: {
const group = searchParams?.get("group"); const group = searchParams?.get("group");
const [totalContent, setTotalContent] = useState(); const [totalContent, setTotalContent] = useState();
const isRegional = group || asPath.includes("/polda/");
const searchType = isRegional ? "regional" : "filter";
useEffect(() => { useEffect(() => {
getDataAll(); if (searchType === "regional") {
getDataRegional();
} else {
getDataAll();
}
}, [ }, [
title, title,
categoryFilter, categoryFilter,
@ -56,6 +63,31 @@ export default function FilterVideoComponent(props: {
sortByOpt, sortByOpt,
]); ]);
async function getDataRegional() {
let startDateFilter = startDateString ? startDateString : "";
let endDateFilter = endDateString ? endDateString : "";
let monthFilter = monthYearFilter ? monthYearFilter : "";
const response = await listDataRegional(
"2",
"",
"",
"",
"",
startDateFilter,
endDateFilter,
monthFilter
? getOnlyMonthAndYear(monthFilter)?.split("/")[0]?.replace("0", "")
: "",
monthFilter ? getOnlyMonthAndYear(monthFilter)?.split("/")[1] : ""
);
const data = response?.data?.data;
const contentData = data?.content;
setNewContent(contentData);
setTotalData(data?.totalElements);
// setLoading(false);
}
async function getDataAll() { async function getDataAll() {
if (asPath?.includes("/polda/") == true) { if (asPath?.includes("/polda/") == true) {
if (asPath?.split("/")[2] !== "[polda_name]") { if (asPath?.split("/")[2] !== "[polda_name]") {
@ -137,7 +169,7 @@ export default function FilterVideoComponent(props: {
return newContent?.length > 0 ? ( return newContent?.length > 0 ? (
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<p>{`Audio Visual(${totalContent})`}</p> <p>{`Audio Visual (${totalContent})`}</p>
<Carousel className="w-full max-w-7xl mx-auto"> <Carousel className="w-full max-w-7xl mx-auto">
<CarouselContent> <CarouselContent>
{newContent?.map((video: any) => ( {newContent?.map((video: any) => (
@ -165,9 +197,35 @@ export default function FilterVideoComponent(props: {
</CarouselItem> </CarouselItem>
))} ))}
</CarouselContent> </CarouselContent>
<CarouselPrevious /> <CarouselPrevious className="-ml-0" />
<CarouselNext /> <CarouselNext className="-mr-0" />
</Carousel> </Carousel>
<div className="flex justify-center mt-1 mb-6">
<Link
href={`${
asPath.includes("/polda/")
? `/${asPath.split("/")[1]}/${asPath.split("/")[2]}`
: ""
}/video/${
group || asPath.includes("/polda/") ? "regional?" : "filter?"
}${sortByOpt === "popular" ? "sortBy=popular" : "sortBy=latest"}${
title ? `&title=${title.toLowerCase()}` : ""
}${categorie ? `&category=${categorie}` : ""}${
startDateString ? `&startDate=${startDateString}` : ""
}${endDateString ? `&endDate=${endDateString}` : ""}${
monthYearFilter
? `&month=${getOnlyMonthAndYear(monthYearFilter)?.split("/")[0]}`
: ""
}${
monthYearFilter
? `&year=${getOnlyMonthAndYear(monthYearFilter)?.split("/")[1]}`
: ""
}`}
className="border border-red-500 text-red-500 hover:bg-red-500 hover:text-white px-4 py-2 text-sm rounded transition duration-200"
>
Lihat Semua
</Link>
</div>
</div> </div>
) : ( ) : (
"" ""

View File

@ -1,6 +1,5 @@
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import router from "next/router";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Icon } from "@iconify/react/dist/iconify.js"; import { Icon } from "@iconify/react/dist/iconify.js";
import { import {
@ -13,7 +12,6 @@ import {
} from "../ui/select"; } from "../ui/select";
import Image from "next/image"; import Image from "next/image";
import { getHeroData } from "@/service/landing/landing"; import { getHeroData } from "@/service/landing/landing";
import { title } from "process";
import { htmlToString } from "@/utils/globals"; import { htmlToString } from "@/utils/globals";
import { Link, useRouter } from "@/i18n/routing"; import { Link, useRouter } from "@/i18n/routing";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
@ -88,7 +86,7 @@ const ScrollableContent = () => {
<span className="text-[#c03724] dark:text-white"> <span className="text-[#c03724] dark:text-white">
&nbsp;{t("download", { defaultValue: "Download" })}&nbsp; &nbsp;{t("download", { defaultValue: "Download" })}&nbsp;
{t("coverage", { defaultValue: "Coverage" })} {t("coverage", { defaultValue: "Coverage" })}
</span>{" "} </span>
</h1> </h1>
<div className="w-[10%] h-1 bg-[#bb3523] mt-2"></div> <div className="w-[10%] h-1 bg-[#bb3523] mt-2"></div>
<div className="w-full h-1 bg-[#bb3523] mx-auto"></div> <div className="w-full h-1 bg-[#bb3523] mx-auto"></div>

View File

@ -27,7 +27,6 @@ const SearchSection = () => {
const t = useTranslations("LandingPage"); const t = useTranslations("LandingPage");
const { theme } = useTheme(); const { theme } = useTheme();
// Determine background image based on theme
const getBackgroundImage = () => { const getBackgroundImage = () => {
if (theme === "dark") { if (theme === "dark") {
return "url('/assets/background-dark.png')"; return "url('/assets/background-dark.png')";

View File

@ -298,7 +298,7 @@ const DetailAudio = () => {
async function shareToEmail() { async function shareToEmail() {
if (Number(userRoleId) < 1 || userRoleId == undefined) { if (Number(userRoleId) < 1 || userRoleId == undefined) {
router.push("/auth/login"); router.push("/auth");
} else { } else {
const data = { const data = {
mediaUploadId: id?.split("-")?.[0], mediaUploadId: id?.split("-")?.[0],

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import { useLocale } from "next-intl"; import { useLocale } from "next-intl";
import { useParams } from "next/navigation"; import { useParams, useSearchParams } from "next/navigation";
import { locales } from "@/config"; import { locales } from "@/config";
import { usePathname, useRouter } from "@/i18n/routing"; import { usePathname, useRouter } from "@/i18n/routing";
@ -33,13 +33,22 @@ export default function LocalSwitcher() {
const params = useParams(); const params = useParams();
const localActive = useLocale() || "in"; const localActive = useLocale() || "in";
const [selectedLang, setSelectedLang] = useState<string>(""); const [selectedLang, setSelectedLang] = useState<string>("");
const searchParams = useSearchParams();
useEffect(() => { useEffect(() => {
const storedLang = getLanguage(); const storedLang = getLanguage();
let joinParam = "";
if (searchParams) {
joinParam = Array.from(searchParams.entries())
.map(([key, value]) => `${key}=${value}`)
.join("&");
}
if (pathname.includes("polda")){ if (pathname.includes("polda")) {
startTransition(() => { startTransition(() => {
router.replace(pathname, { locale: "in" }); router.replace(pathname + joinParam === "" ? "" : `?${joinParam}`, {
locale: "in",
});
}); });
} else { } else {
if (!storedLang) { if (!storedLang) {
@ -47,16 +56,20 @@ export default function LocalSwitcher() {
setSelectedLang("in"); setSelectedLang("in");
startTransition(() => { startTransition(() => {
router.replace(pathname, { locale: "in" }); router.replace(pathname + joinParam === "" ? "" : `?${joinParam}`, {
locale: "in",
});
}); });
} else { } else {
setSelectedLang(storedLang); setSelectedLang(storedLang);
startTransition(() => { startTransition(() => {
router.replace(pathname, { locale: storedLang }); router.replace(pathname + joinParam === "" ? "" : `?${joinParam}`, {
locale: storedLang,
});
}); });
} }
} }
}, []); }, [searchParams]);
const onSelectChange = (nextLocale: string) => { const onSelectChange = (nextLocale: string) => {
setLanguage(nextLocale); setLanguage(nextLocale);

View File

@ -9,7 +9,7 @@ import {
AuthState, AuthState,
AuthContextType, AuthContextType,
EmailValidationData, EmailValidationData,
OTPData OTPData,
} from "@/types/auth"; } from "@/types/auth";
import { import {
login, login,
@ -17,7 +17,7 @@ import {
postEmailValidation, postEmailValidation,
postSetupEmail, postSetupEmail,
verifyOTPByUsername, verifyOTPByUsername,
doLogin doLogin,
} from "@/service/auth"; } from "@/service/auth";
import { import {
setAuthCookies, setAuthCookies,
@ -29,7 +29,7 @@ import {
showAuthError, showAuthError,
showAuthSuccess, showAuthSuccess,
loginRateLimiter, loginRateLimiter,
AUTH_CONSTANTS AUTH_CONSTANTS,
} from "@/lib/auth-utils"; } from "@/lib/auth-utils";
import { warning } from "@/lib/swal"; import { warning } from "@/lib/swal";
@ -46,108 +46,114 @@ export const useAuth = (): AuthContextType => {
useEffect(() => { useEffect(() => {
const checkAuth = async () => { const checkAuth = async () => {
try { try {
setState(prev => ({ ...prev, loading: true })); setState((prev) => ({ ...prev, loading: true }));
// Add logic to check if user is authenticated // Add logic to check if user is authenticated
// This could check for valid tokens, etc. // This could check for valid tokens, etc.
} catch (error) { } catch (error) {
setState(prev => ({ setState((prev) => ({
...prev, ...prev,
isAuthenticated: false, isAuthenticated: false,
user: null, user: null,
error: "Authentication check failed" error: "Authentication check failed",
})); }));
} finally { } finally {
setState(prev => ({ ...prev, loading: false })); setState((prev) => ({ ...prev, loading: false }));
} }
}; };
checkAuth(); checkAuth();
}, []); }, []);
const login = useCallback(async (credentials: LoginFormData): Promise<void> => { const login = useCallback(
try { async (credentials: LoginFormData): Promise<void> => {
setState(prev => ({ ...prev, loading: true, error: null })); try {
setState((prev) => ({ ...prev, loading: true, error: null }));
// Check rate limiting // Check rate limiting
if (!loginRateLimiter.canAttempt(credentials.username)) { if (!loginRateLimiter.canAttempt(credentials.username)) {
const remainingTime = loginRateLimiter.getRemainingTime(credentials.username); const remainingTime = loginRateLimiter.getRemainingTime(
const minutes = Math.ceil(remainingTime / (60 * 1000)); credentials.username
throw new Error(`Too many login attempts. Please try again in ${minutes} minutes.`); );
const minutes = Math.ceil(remainingTime / (60 * 1000));
throw new Error(
`Too many login attempts. Please try again in ${minutes} minutes.`
);
}
// Attempt login
const response = await doLogin({
...credentials,
grantType: AUTH_CONSTANTS.GRANT_TYPE,
clientId: AUTH_CONSTANTS.CLIENT_ID,
});
if (response?.error) {
loginRateLimiter.recordAttempt(credentials.username);
throw new Error("Invalid username or password");
}
const { access_token, refresh_token } = response?.data || {};
if (!access_token || !refresh_token) {
throw new Error("Invalid response from server");
}
// Set auth cookies
setAuthCookies(access_token, refresh_token);
// Get user profile
const profileResponse = await getProfile(access_token);
const profile: ProfileData = profileResponse?.data?.data;
if (!profile) {
throw new Error("Failed to fetch user profile");
}
// Validate user eligibility
// if (!isUserEligible(profile)) {
// clearAllCookies();
// warning(
// "Akun Anda tidak dapat digunakan untuk masuk ke MediaHub Polri",
// "/auth"
// );
// return;
// }
// Set profile cookies
setProfileCookies(profile);
// Reset rate limiter on successful login
loginRateLimiter.resetAttempts(credentials.username);
// Navigate based on user role
const navigationPath = getNavigationPath(
profile.roleId,
profile.userLevel?.id,
profile.userLevel?.parentLevelId
);
// Update state
setState({
isAuthenticated: true,
user: profile,
loading: false,
error: null,
});
// Navigate to appropriate dashboard
window.location.href = navigationPath;
} catch (error: any) {
const errorMessage = error?.message || "Login failed";
setState((prev) => ({
...prev,
loading: false,
error: errorMessage,
}));
showAuthError(error, "Login failed");
} }
},
// Attempt login [router]
const response = await doLogin({ );
...credentials,
grantType: AUTH_CONSTANTS.GRANT_TYPE,
clientId: AUTH_CONSTANTS.CLIENT_ID,
});
if (response?.error) {
loginRateLimiter.recordAttempt(credentials.username);
throw new Error("Invalid username or password");
}
const { access_token, refresh_token } = response?.data || {};
if (!access_token || !refresh_token) {
throw new Error("Invalid response from server");
}
// Set auth cookies
setAuthCookies(access_token, refresh_token);
// Get user profile
const profileResponse = await getProfile(access_token);
const profile: ProfileData = profileResponse?.data?.data;
if (!profile) {
throw new Error("Failed to fetch user profile");
}
// Validate user eligibility
// if (!isUserEligible(profile)) {
// clearAllCookies();
// warning(
// "Akun Anda tidak dapat digunakan untuk masuk ke MediaHub Polri",
// "/auth"
// );
// return;
// }
// Set profile cookies
setProfileCookies(profile);
// Reset rate limiter on successful login
loginRateLimiter.resetAttempts(credentials.username);
// Navigate based on user role
const navigationPath = getNavigationPath(
profile.roleId,
profile.userLevel?.id,
profile.userLevel?.parentLevelId
);
// Update state
setState({
isAuthenticated: true,
user: profile,
loading: false,
error: null,
});
// Navigate to appropriate dashboard
window.location.href = navigationPath;
} catch (error: any) {
const errorMessage = error?.message || "Login failed";
setState(prev => ({
...prev,
loading: false,
error: errorMessage
}));
showAuthError(error, "Login failed");
}
}, [router]);
const logout = useCallback((): void => { const logout = useCallback((): void => {
clearAllCookies(); clearAllCookies();
@ -162,13 +168,13 @@ export const useAuth = (): AuthContextType => {
const refreshToken = useCallback(async (): Promise<void> => { const refreshToken = useCallback(async (): Promise<void> => {
try { try {
setState(prev => ({ ...prev, loading: true })); setState((prev) => ({ ...prev, loading: true }));
// Add token refresh logic here // Add token refresh logic here
// This would typically call an API to refresh the access token // This would typically call an API to refresh the access token
} catch (error) { } catch (error) {
logout(); logout();
} finally { } finally {
setState(prev => ({ ...prev, loading: false })); setState((prev) => ({ ...prev, loading: false }));
} }
}, [logout]); }, [logout]);
@ -185,38 +191,43 @@ export const useEmailValidation = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const validateEmail = useCallback(async (credentials: LoginFormData): Promise<string> => { const validateEmail = useCallback(
try { async (credentials: LoginFormData): Promise<string> => {
setLoading(true); try {
setError(null); setLoading(true);
setError(null);
const response = await postEmailValidation(credentials); const response = await postEmailValidation(credentials);
if (response?.error) { if (response?.error) {
throw new Error(response?.message || "Email validation failed"); throw new Error(response?.message || "Email validation failed");
}
const message = response?.data?.message;
switch (message) {
case "Continue to setup email":
return "setup";
case "Email is valid and OTP has been sent":
return "otp";
case "Username & password valid":
return "success";
case "Skip to login":
return "skip";
default:
return "login";
}
} catch (error: any) {
const errorMessage = error?.message || "Email validation failed";
setError(errorMessage);
showAuthError(error, "Email validation failed");
throw error;
} finally {
setLoading(false);
} }
},
const message = response?.data?.message; []
);
switch (message) {
case "Continue to setup email":
return "setup";
case "Email is valid and OTP has been sent":
return "otp";
case "Username & password valid":
return "success";
default:
return "login";
}
} catch (error: any) {
const errorMessage = error?.message || "Email validation failed";
setError(errorMessage);
showAuthError(error, "Email validation failed");
throw error;
} finally {
setLoading(false);
}
}, []);
return { return {
validateEmail, validateEmail,
@ -230,46 +241,49 @@ export const useEmailSetup = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const setupEmail = useCallback(async ( const setupEmail = useCallback(
credentials: LoginFormData, async (
emailData: EmailValidationData credentials: LoginFormData,
): Promise<string> => { emailData: EmailValidationData
try { ): Promise<string> => {
setLoading(true); try {
setError(null); setLoading(true);
setError(null);
const data = { const data = {
username: credentials.username, username: credentials.username,
password: credentials.password, password: credentials.password,
oldEmail: emailData.oldEmail, oldEmail: emailData.oldEmail,
newEmail: emailData.newEmail, newEmail: emailData.newEmail,
}; };
const response = await postSetupEmail(data); const response = await postSetupEmail(data);
if (response?.error) { if (response?.error) {
throw new Error(response.message || "Email setup failed"); throw new Error(response.message || "Email setup failed");
}
const message = response?.data?.message;
switch (message) {
case "Email is valid and OTP has been sent":
return "otp";
case "The old email is not same":
throw new Error("Email is invalid");
default:
return "success";
}
} catch (error: any) {
const errorMessage = error?.message || "Email setup failed";
setError(errorMessage);
showAuthError(error, "Email setup failed");
throw error;
} finally {
setLoading(false);
} }
},
const message = response?.data?.message; []
);
switch (message) {
case "Email is valid and OTP has been sent":
return "otp";
case "The old email is not same":
throw new Error("Email is invalid");
default:
return "success";
}
} catch (error: any) {
const errorMessage = error?.message || "Email setup failed";
setError(errorMessage);
showAuthError(error, "Email setup failed");
throw error;
} finally {
setLoading(false);
}
}, []);
return { return {
setupEmail, setupEmail,
@ -283,34 +297,34 @@ export const useOTPVerification = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const verifyOTP = useCallback(async ( const verifyOTP = useCallback(
username: string, async (username: string, otp: string): Promise<boolean> => {
otp: string try {
): Promise<boolean> => { setLoading(true);
try { setError(null);
setLoading(true);
setError(null);
if (otp.length !== 6) { if (otp.length !== 6) {
throw new Error("OTP must be exactly 6 digits"); throw new Error("OTP must be exactly 6 digits");
}
const response = await verifyOTPByUsername(username, otp);
if (response?.error) {
throw new Error(response.message || "OTP verification failed");
}
return response?.message === "success";
} catch (error: any) {
const errorMessage = error?.message || "OTP verification failed";
setError(errorMessage);
showAuthError(error, "OTP verification failed");
throw error;
} finally {
setLoading(false);
} }
},
const response = await verifyOTPByUsername(username, otp); []
);
if (response?.error) {
throw new Error(response.message || "OTP verification failed");
}
return response?.message === "success";
} catch (error: any) {
const errorMessage = error?.message || "OTP verification failed";
setError(errorMessage);
showAuthError(error, "OTP verification failed");
throw error;
} finally {
setLoading(false);
}
}, []);
return { return {
verifyOTP, verifyOTP,

View File

@ -641,6 +641,9 @@
"timeTable1": "TIMETABLE /" "timeTable1": "TIMETABLE /"
}, },
"FilterPage": { "FilterPage": {
"downloadableContent": "Downloadable Content",
"content": "Content",
"allContent":"All Content",
"image": "Image", "image": "Image",
"video": "Video", "video": "Video",
"text": "Text", "text": "Text",
@ -653,10 +656,10 @@
"searchTitle": "Find Title...", "searchTitle": "Find Title...",
"monthYear": "Month and Year", "monthYear": "Month and Year",
"thereIs": "There is", "thereIs": "There is",
"downloadableImage": "downloadable image", "downloadableImage": "Downloadable image",
"downloadableVideo": "downloadable video", "downloadableVideo": "Downloadable video",
"downloadableText": "downloadable text", "downloadableText": "Downloadable text",
"downloadableAudio": "downloadable audio", "downloadableAudio": "Downloadable audio",
"date": "Date", "date": "Date",
"selectYear": "Select Month and Year", "selectYear": "Select Month and Year",
"selectDate": "Select Date", "selectDate": "Select Date",

View File

@ -642,6 +642,9 @@
"timeTable1": "JADWAL /" "timeTable1": "JADWAL /"
}, },
"FilterPage": { "FilterPage": {
"content": "Konten",
"downloadableContent": "artikel berisi konten yang dapat di unduh",
"allContent": "Semua Konten",
"image": "Foto", "image": "Foto",
"video": "Audio Visual", "video": "Audio Visual",
"text": "Teks", "text": "Teks",