Initial commit

This commit is contained in:
Anang Yusman 2025-09-16 16:29:07 +08:00
commit 55ac4b20ca
10745 changed files with 1849308 additions and 0 deletions

41
.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

36
README.md Normal file
View File

@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

View File

@ -0,0 +1,307 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { format } from "date-fns";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { error } from "@/lib/swal";
import Link from "next/link";
import { deleteMedia } from "@/service/content";
const useTableColumns = () => {
const MySwal = withReactContent(Swal);
const userLevelId = getCookiesDecrypt("ulie");
const columns: ColumnDef<any>[] = [
{
accessorKey: "no",
header: "No",
cell: ({ row }) => (
<div className="flex items-center gap-5">
<div className="flex-1 text-start">
<h4 className="text-sm font-medium text-default-600 whitespace-nowrap mb-1">
{row.getValue("no")}
</h4>
</div>
</div>
),
},
{
accessorKey: "title",
header: "title",
cell: ({ row }: { row: { getValue: (key: string) => string } }) => {
const title: string = row.getValue("title");
return (
<span className="whitespace-nowrap">
{title.length > 50 ? `${title.slice(0, 30)}...` : title}
</span>
);
},
},
{
accessorKey: "categoryName",
header: "Category Name",
cell: ({ row }) => (
<span className="whitespace-nowrap">
{row.getValue("categoryName")}
</span>
),
},
{
accessorKey: "createdAt",
header: "Upload Date",
cell: ({ row }) => {
const createdAt = row.getValue("createdAt") as
| string
| number
| undefined;
const formattedDate =
createdAt && !isNaN(new Date(createdAt).getTime())
? format(new Date(createdAt), "dd-MM-yyyy HH:mm:ss")
: "-";
return <span className="whitespace-nowrap">{formattedDate}</span>;
},
},
{
accessorKey: "creatorName",
header: "Creator Group",
cell: ({ row }) => (
<span className="whitespace-nowrap">{row.getValue("creatorName")}</span>
),
},
{
accessorKey: "creatorGroupLevelName",
header: "Source",
cell: ({ row }) => (
<span className="whitespace-nowrap">
{row.getValue("creatorGroupLevelName")}
</span>
),
},
{
accessorKey: "publishedOn",
header: "Published",
cell: ({ row }) => {
const isPublish = row.original.isPublish;
const isPublishOnPolda = row.original.isPublishOnPolda;
const creatorGroupParentLevelId =
row.original.creatorGroupParentLevelId;
let displayText = "-";
if (isPublish && !isPublishOnPolda) {
displayText = "Mabes";
} else if (isPublish && isPublishOnPolda) {
if (Number(creatorGroupParentLevelId) == 761) {
displayText = "Mabes & Satker";
} else {
displayText = "Mabes & Polda";
}
} else if (!isPublish && isPublishOnPolda) {
if (Number(creatorGroupParentLevelId) == 761) {
displayText = "Satker";
} else {
displayText = "Polda";
}
}
return (
<div className="text-center whitespace-nowrap" title={displayText}>
{displayText}
</div>
);
},
},
{
accessorKey: "statusName",
header: "Status",
cell: ({ row }) => {
const statusColors: Record<string, string> = {
diterima: "bg-green-100 text-green-600",
"menunggu review": "bg-orange-100 text-orange-600",
};
const colors = [
"bg-orange-100 text-orange-600",
"bg-orange-100 text-orange-600",
"bg-green-100 text-green-600",
"bg-blue-100 text-blue-600",
"bg-red-200 text-red-600",
];
const status =
Number(row.original?.statusId) == 2 &&
row.original?.reviewedAtLevel !== null &&
!row.original?.reviewedAtLevel?.includes(`:${userLevelId}:`) &&
Number(row.original?.creatorGroupLevelId) != Number(userLevelId)
? "1"
: row.original?.statusId;
const statusStyles =
colors[Number(status)] || "bg-red-200 text-red-600";
// const statusStyles = statusColors[status] || "bg-red-200 text-red-600";
return (
<Badge
className={cn(
"rounded-full px-5 w-full whitespace-nowrap",
statusStyles
)}
>
{(Number(row.original?.statusId) == 2 &&
!row.original?.reviewedAtLevel !== null &&
!row.original?.reviewedAtLevel?.includes(
`:${Number(userLevelId)}:`
) &&
Number(row.original?.creatorGroupLevelId) !=
Number(userLevelId)) ||
(Number(row.original?.statusId) == 1 &&
Number(row.original?.needApprovalFromLevel) ==
Number(userLevelId))
? "Menunggu Review"
: row.original?.statusName}{" "}
</Badge>
);
},
},
{
id: "actions",
accessorKey: "action",
header: "Action",
enableHiding: false,
cell: ({ row }) => {
const MySwal = withReactContent(Swal);
async function doDelete(id: any) {
// loading();
const data = {
id,
};
const response = await deleteMedia(data);
if (response?.error) {
error(response.message);
return false;
}
success();
}
function success() {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
window.location.reload();
}
});
}
const handleDeleteMedia = (id: any) => {
MySwal.fire({
title: "Hapus Data",
text: "",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#3085d6",
confirmButtonColor: "#d33",
confirmButtonText: "Hapus",
}).then((result) => {
if (result.isConfirmed) {
doDelete(id);
}
});
};
const [isMabesApprover, setIsMabesApprover] = React.useState(false);
const userId = getCookiesDecrypt("uie");
const userLevelId = getCookiesDecrypt("ulie");
const roleId = getCookiesDecrypt("urie");
React.useEffect(() => {
if (userLevelId !== undefined && roleId !== undefined) {
setIsMabesApprover(
Number(userLevelId) == 216 && Number(roleId) == 3
);
}
}, [userLevelId, roleId]);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
>
<span className="sr-only">Open menu</span>
<MoreVertical className="h-4 w-4 text-default-800" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0" align="end">
<Link
href={`/admin/content/audio-visual/detail/${row.original.id}`}
>
<DropdownMenuItem className="p-2 border-b text-default-700 group rounded-none">
<Eye className="w-4 h-4 me-1.5" />
View
</DropdownMenuItem>
</Link>
{/* <Link
href={`/contributor/content/video/update/${row.original.id}`}
>
<DropdownMenuItem className="p-2 border-b text-default-700 group rounded-none">
<SquarePen className="w-4 h-4 me-1.5" />
Edit
</DropdownMenuItem>
</Link> */}
{(Number(row.original.uploadedById) === Number(userId) ||
isMabesApprover) && (
<Link
href={`/admin/content/audio-visual/update/${row.original.id}`}
>
<DropdownMenuItem className="p-2 border-b text-default-700 group rounded-none">
<SquarePen className="w-4 h-4 me-1.5" />
Edit
</DropdownMenuItem>
</Link>
)}
<DropdownMenuItem
onClick={() => handleDeleteMedia(row.original.id)}
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-white rounded-none"
>
<Trash2 className="w-4 h-4 me-1.5 focus:text-white" />
Delete
</DropdownMenuItem>
{/* {(row.original.uploadedById === userId || isMabesApprover) && (
<DropdownMenuItem
onClick={() => handleDeleteMedia(row.original.id)}
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-destructive-foreground rounded-none"
>
<Trash2 className="w-4 h-4 me-1.5" />
Hapus
</DropdownMenuItem>
)} */}
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
return columns;
};
export default useTableColumns;

View File

@ -0,0 +1,491 @@
"use client";
import * as React from "react";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
ChevronDown,
ChevronLeft,
ChevronRight,
Eye,
MoreVertical,
Search,
SquarePen,
Trash2,
TrendingDown,
TrendingUp,
} from "lucide-react";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { useRouter, useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination";
import columns from "./columns";
import { Label } from "@/components/ui/label";
import { format } from "date-fns";
import useTableColumns from "./columns";
import { listEnableCategory, listDataVideo } from "@/service/content";
import {
SortingState,
ColumnFiltersState,
VisibilityState,
PaginationState,
useReactTable,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
getFilteredRowModel,
flexRender,
} from "@tanstack/react-table";
const TableVideo = () => {
const router = useRouter();
const searchParams = useSearchParams();
const [dataTable, setDataTable] = React.useState<any[]>([]);
const [totalData, setTotalData] = 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 [showData, setShowData] = React.useState("50");
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: Number(showData),
});
const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1);
const [limit, setLimit] = React.useState(10);
const [search, setSearch] = React.useState<string>("");
const userId = getCookiesDecrypt("uie");
const userLevelId = getCookiesDecrypt("ulie");
const [categories, setCategories] = React.useState<any[]>([]);
const [selectedCategories, setSelectedCategories] = React.useState<number[]>(
[]
);
const [categoryFilter, setCategoryFilter] = React.useState<string>("");
const [statusFilter, setStatusFilter] = React.useState<any[]>([]);
const [startDate, setStartDate] = React.useState("");
const [endDate, setEndDate] = React.useState("");
const [filterByCreator, setFilterByCreator] = React.useState("");
const [filterBySource, setFilterBySource] = React.useState("");
const [filterByCreatorGroup, setFilterByCreatorGroup] = React.useState("");
const roleId = getCookiesDecrypt("urie");
const columns = useTableColumns();
const table = useReactTable({
data: dataTable,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
React.useEffect(() => {
fetchData();
getCategories();
}, [
categoryFilter,
statusFilter,
page,
showData,
,
search,
startDate,
endDate,
]);
async function getCategories() {
const category = await listEnableCategory("2");
const resCategory = category?.data?.data?.content;
setCategories(resCategory || []);
}
// Fungsi menangani perubahan checkbox
const handleCheckboxChange = (categoryId: number) => {
setSelectedCategories(
(prev: any) =>
prev.includes(categoryId)
? prev.filter((id: any) => id !== categoryId) // Hapus jika sudah dipilih
: [...prev, categoryId] // Tambahkan jika belum dipilih
);
// Perbarui filter kategori
setCategoryFilter((prev) => {
const updatedCategories = prev.split(",").filter(Boolean).map(Number);
const newCategories = updatedCategories.includes(categoryId)
? updatedCategories.filter((id) => id !== categoryId)
: [...updatedCategories, categoryId];
return newCategories.join(",");
});
};
async function fetchData() {
const formattedStartDate = startDate
? format(new Date(startDate), "yyyy-MM-dd")
: "";
const formattedEndDate = endDate
? format(new Date(endDate), "yyyy-MM-dd")
: "";
try {
const isForSelf = Number(roleId) === 4;
const res = await listDataVideo(
showData,
page - 1,
isForSelf,
!isForSelf,
categoryFilter,
statusFilter,
statusFilter?.sort().join(",").includes("1") ? userLevelId : "",
filterByCreator,
filterBySource,
formattedStartDate, // Pastikan format sesuai
formattedEndDate, // Pastikan format sesuai
search,
filterByCreatorGroup
);
const data = res?.data?.data;
const contentData = data?.content;
contentData.forEach((item: any, index: number) => {
item.no = (page - 1) * Number(showData) + index + 1;
});
setDataTable(contentData);
setTotalData(data?.totalElements);
setTotalPage(data?.totalPages);
} catch (error) {
console.error("Error fetching tasks:", error);
}
}
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value); // Perbarui state search
table.getColumn("title")?.setFilterValue(e.target.value); // Set filter tabel
};
const handleSearchFilterBySource = (
e: React.ChangeEvent<HTMLInputElement>
) => {
const value = e.target.value;
setFilterBySource(value); // Perbarui state filter
fetchData(); // Panggil ulang data dengan filter baru
};
function handleStatusCheckboxChange(value: any) {
setStatusFilter((prev: any) =>
prev.includes(value)
? prev.filter((status: any) => status !== value)
: [...prev, value]
);
}
const handleSearchFilterByCreator = (
e: React.ChangeEvent<HTMLInputElement>
) => {
const value = e.target.value;
setFilterByCreator(value); // Perbarui state filter
fetchData(); // Panggil ulang data dengan filter baru
};
return (
<div className="w-full overflow-x-auto">
<div className="flex flex-col md:flex-row lg:flex-row md:justify-between lg:justify-between items-center md:px-5 lg:px-5">
<div className="relative w-full md:w-[200px] lg:w-[200px] px-2">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400 dark:text-white" />
<Input
type="text"
placeholder="Search Judul..."
className="pl-9 bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white"
value={search}
onChange={handleSearch}
/>
</div>
<div className="flex flex-row items-center gap-3">
<div className="flex items-center py-4">
<div className="mx-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="md" variant="outline">
{showData} Data
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 text-sm">
<DropdownMenuRadioGroup
value={showData}
onValueChange={setShowData}
>
<DropdownMenuRadioItem value="10">
10 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="50">
50 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="100">
100 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="250">
250 Data
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto" size="md">
Filter <ChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-64 h-[200px] overflow-y-auto"
>
<div className="flex flex-row justify-between my-1 mx-1">
<p>Filter</p>
{/* <p
className="text-blue-600 cursor-pointer"
onClick={fetchData}
>
Simpan
</p> */}
</div>
<Label className="ml-2">Kategori</Label>
{categories.length > 0 ? (
categories.map((category) => (
<div
key={category.id}
className="flex items-center px-4 py-1"
>
<input
type="checkbox"
id={`category-${category.id}`}
className="mr-2"
checked={selectedCategories.includes(category.id)}
onChange={() => handleCheckboxChange(category.id)}
/>
<label
htmlFor={`category-${category.id}`}
className="text-sm"
>
{category.name}
</label>
</div>
))
) : (
<p className="text-sm text-gray-500 px-4 py-2">
No categories found.
</p>
)}
<div className="mx-2 my-1">
<Label>Tanggal Awal</Label>
<Input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="max-w-sm"
/>
</div>
<div className="mx-2 my-1">
<Label>Tanggal Akhir</Label>
<Input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="max-w-sm"
/>
</div>
<div className="mx-2 my-1">
<Label>Kreator</Label>
<Input
placeholder="Filter Status..."
value={filterByCreator}
onChange={handleSearchFilterByCreator}
className="max-w-sm"
/>
</div>
<div className="mx-2 my-1">
<Label>Sumber</Label>
<Input
placeholder="Filter Status..."
value={filterBySource}
onChange={handleSearchFilterBySource}
className="max-w-sm"
/>
</div>
<Label className="ml-2 mt-2">Status</Label>
<div className="flex items-center px-4 py-1">
<input
type="checkbox"
id="status-2"
className="mr-2"
checked={statusFilter.includes(1)}
onChange={() => handleStatusCheckboxChange(1)}
/>
<label htmlFor="status-2" className="text-sm">
Menunggu Review
</label>
</div>
<div className="flex items-center px-4 py-1">
<input
type="checkbox"
id="status-2"
className="mr-2"
checked={statusFilter.includes(2)}
onChange={() => handleStatusCheckboxChange(2)}
/>
<label htmlFor="status-2" className="text-sm">
Diterima
</label>
</div>
<div className="flex items-center px-4 py-1">
<input
type="checkbox"
id="status-3"
className="mr-2"
checked={statusFilter.includes(3)}
onChange={() => handleStatusCheckboxChange(3)}
/>
<label htmlFor="status-3" className="text-sm">
Minta Update
</label>
</div>
<div className="flex items-center px-4 py-1">
<input
type="checkbox"
id="status-4"
className="mr-2"
checked={statusFilter.includes(4)}
onChange={() => handleStatusCheckboxChange(4)}
/>
<label htmlFor="status-4" className="text-sm">
Ditolak
</label>
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex items-center py-4">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto" size="md">
Columns <ChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
<Table className="overflow-hidden mt-3 mx-3">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-default-200">
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="h-[75px]"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<TablePagination
table={table}
totalData={totalData}
totalPage={totalPage}
/>
</div>
);
};
export default TableVideo;

View File

@ -0,0 +1,14 @@
import FormVideo from "@/components/form/content/audio-visual/video-form";
import SiteBreadcrumb from "@/components/site-breadcrumb";
const VideoCreatePage = async () => {
return (
<div>
<div className="space-y-4 m-3">
<FormVideo />
</div>
</div>
);
};
export default VideoCreatePage;

View File

@ -0,0 +1,14 @@
import FormVideoDetail from "@/components/form/content/audio-visual/video-detail-form";
const VideoDetailPage = async () => {
return (
<div>
{/* <SiteBreadcrumb /> */}
<div className="space-y-4">
<FormVideoDetail />
</div>
</div>
);
};
export default VideoDetailPage;

View File

@ -0,0 +1,11 @@
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Media Hub | POLRI",
description: "Media Hub merupakan situs resmi milik Divisi Humas Polri di mana di dalamnya berisi konten-konten yang dapat diakses secara gratis oleh Internal Polri, Jurnalis, Masyarakat Umum, dan KSP.",
};
const Layout = ({ children }: { children: React.ReactNode }) => {
return <>{children}</>;
};
export default Layout;

View File

@ -0,0 +1,48 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { UploadIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Icon } from "@iconify/react/dist/iconify.js";
import Link from "next/link";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import TableVideo from "./components/table-video";
const ReactTableImagePage = () => {
return (
<div>
{/* <SiteBreadcrumb /> */}
<div className="space-y-4">
<Card className="m-2">
<CardHeader className="border-b border-solid border-default-200 mb-6">
<CardTitle>
<div className="flex items-center">
<div className="flex-1 text-xl font-medium text-default-900">
Audio Visual
</div>
<div className="flex-none">
<Link href={"/admin/content/audio-visual/create"}>
<Button color="primary" className="text-white">
<UploadIcon size={18} className="mr-2" />
Audio Visual
</Button>
</Link>
{/* <Link href={"/contributor/content/image/createAi"}>
<Button color="primary" className="text-white ml-3">
<UploadIcon />
Unggah Foto Dengan AI
</Button>
</Link> */}
</div>
</div>
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<TableVideo />
</CardContent>
</Card>
</div>
</div>
);
};
export default ReactTableImagePage;

View File

@ -0,0 +1,14 @@
import FormVideoUpdate from "@/components/form/content/audio-visual/video-update-form";
const VideoUpdatePage = async () => {
return (
<div>
{/* <SiteBreadcrumb /> */}
<div className="space-y-4">
<FormVideoUpdate />
</div>
</div>
);
};
export default VideoUpdatePage;

View File

@ -0,0 +1,302 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { format } from "date-fns";
import withReactContent from "sweetalert2-react-content";
import { deleteMedia } from "@/service/content/content";
import { error } from "@/lib/swal";
import Swal from "sweetalert2";
import Link from "next/link";
const useTableColumns = () => {
const MySwal = withReactContent(Swal);
const userLevelId = getCookiesDecrypt("ulie");
const columns: ColumnDef<any>[] = [
{
accessorKey: "no",
header: "No",
cell: ({ row }) => (
<div className="flex items-center gap-5">
<div className="flex-1 text-start">
<h4 className="text-sm font-medium text-default-600 whitespace-nowrap mb-1">
{row.getValue("no")}
</h4>
</div>
</div>
),
},
{
accessorKey: "title",
header: "Title",
cell: ({ row }: { row: { getValue: (key: string) => string } }) => {
const title: string = row.getValue("title");
return (
<span className="whitespace-nowrap">
{title.length > 50 ? `${title.slice(0, 30)}...` : title}
</span>
);
},
},
{
accessorKey: "categoryName",
header: "Category Name",
cell: ({ row }) => (
<span className="whitespace-nowrap">
{row.getValue("categoryName")}
</span>
),
},
{
accessorKey: "createdAt",
header: "Upload Date",
cell: ({ row }) => {
const createdAt = row.getValue("createdAt") as
| string
| number
| undefined;
const formattedDate =
createdAt && !isNaN(new Date(createdAt).getTime())
? format(new Date(createdAt), "dd-MM-yyyy HH:mm:ss")
: "-";
return <span className="whitespace-nowrap">{formattedDate}</span>;
},
},
{
accessorKey: "creatorName",
header: "Creator Group",
cell: ({ row }) => (
<span className="whitespace-nowrap">{row.getValue("creatorName")}</span>
),
},
{
accessorKey: "creatorGroupLevelName",
header: "Source",
cell: ({ row }) => (
<span className="whitespace-nowrap">
{row.getValue("creatorGroupLevelName")}
</span>
),
},
{
accessorKey: "publishedOn",
header: "Published",
cell: ({ row }) => {
const isPublish = row.original.isPublish;
const isPublishOnPolda = row.original.isPublishOnPolda;
const creatorGroupParentLevelId =
row.original.creatorGroupParentLevelId;
let displayText = "-";
if (isPublish && !isPublishOnPolda) {
displayText = "Mabes";
} else if (isPublish && isPublishOnPolda) {
if (Number(creatorGroupParentLevelId) == 761) {
displayText = "Mabes & Satker";
} else {
displayText = "Mabes & Polda";
}
} else if (!isPublish && isPublishOnPolda) {
if (Number(creatorGroupParentLevelId) == 761) {
displayText = "Satker";
} else {
displayText = "Polda";
}
}
return (
<div className="text-center whitespace-nowrap" title={displayText}>
{displayText}
</div>
);
},
},
{
accessorKey: "statusName",
header: "Status",
cell: ({ row }) => {
const statusColors: Record<string, string> = {
diterima: "bg-green-100 text-green-600",
"menunggu review": "bg-orange-100 text-orange-600",
};
const colors = [
"bg-orange-100 text-orange-600",
"bg-orange-100 text-orange-600",
"bg-green-100 text-green-600",
"bg-blue-100 text-blue-600",
"bg-red-200 text-red-600",
];
const status =
Number(row.original?.statusId) == 2 &&
row.original?.reviewedAtLevel !== null &&
!row.original?.reviewedAtLevel?.includes(`:${userLevelId}:`) &&
Number(row.original?.creatorGroupLevelId) != Number(userLevelId)
? "1"
: row.original?.statusId;
const statusStyles =
colors[Number(status)] || "bg-red-200 text-red-600";
// const statusStyles = statusColors[status] || "bg-red-200 text-red-600";
return (
<Badge
className={cn(
"rounded-full px-5 w-full whitespace-nowrap",
statusStyles
)}
>
{(Number(row.original?.statusId) == 2 &&
!row.original?.reviewedAtLevel !== null &&
!row.original?.reviewedAtLevel?.includes(
`:${Number(userLevelId)}:`
) &&
Number(row.original?.creatorGroupLevelId) !=
Number(userLevelId)) ||
(Number(row.original?.statusId) == 1 &&
Number(row.original?.needApprovalFromLevel) ==
Number(userLevelId))
? "Menunggu Review"
: row.original?.statusName}{" "}
</Badge>
);
},
},
{
id: "actions",
accessorKey: "action",
header: "Action",
enableHiding: false,
cell: ({ row }) => {
const MySwal = withReactContent(Swal);
async function doDelete(id: any) {
// loading();
const data = {
id,
};
const response = await deleteMedia(data);
if (response?.error) {
error(response.message);
return false;
}
success();
}
function success() {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
window.location.reload();
}
});
}
const handleDeleteMedia = (id: any) => {
MySwal.fire({
title: "Hapus Data",
text: "",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#3085d6",
confirmButtonColor: "#d33",
confirmButtonText: "Hapus",
}).then((result) => {
if (result.isConfirmed) {
doDelete(id);
}
});
};
const [isMabesApprover, setIsMabesApprover] = React.useState(false);
const userId = getCookiesDecrypt("uie");
const userLevelId = getCookiesDecrypt("ulie");
const roleId = getCookiesDecrypt("urie");
React.useEffect(() => {
if (userLevelId !== undefined && roleId !== undefined) {
setIsMabesApprover(
Number(userLevelId) == 216 && Number(roleId) == 3
);
}
}, [userLevelId, roleId]);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
>
<span className="sr-only">Open menu</span>
<MoreVertical className="h-4 w-4 text-default-800" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0" align="end">
<Link href={`/admin/content/audio/detail/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group rounded-none">
<Eye className="w-4 h-4 me-1.5" />
View
</DropdownMenuItem>
</Link>
{/* <Link
href={`/admin/content/audio/update/${row.original.id}`}
>
<DropdownMenuItem className="p-2 border-b text-default-700 group rounded-none">
<SquarePen className="w-4 h-4 me-1.5" />
Edit
</DropdownMenuItem>
</Link> */}
{(Number(row.original.uploadedById) === Number(userId) ||
isMabesApprover) && (
<Link href={`/admin/content/audio/update/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group rounded-none">
<SquarePen className="w-4 h-4 me-1.5" />
Edit
</DropdownMenuItem>
</Link>
)}
<DropdownMenuItem
onClick={() => handleDeleteMedia(row.original.id)}
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-white rounded-none"
>
<Trash2 className="w-4 h-4 me-1.5 focus:text-white" />
Delete
</DropdownMenuItem>
{/* {(row.original.uploadedById === userId || isMabesApprover) && (
<DropdownMenuItem
onClick={() => handleDeleteMedia(row.original.id)}
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-destructive-foreground rounded-none"
>
<Trash2 className="w-4 h-4 me-1.5" />
Hapus
</DropdownMenuItem>
)} */}
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
return columns;
};
export default useTableColumns;

View File

@ -0,0 +1,497 @@
"use client";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
ChevronDown,
ChevronLeft,
ChevronRight,
Eye,
MoreVertical,
Search,
SquarePen,
Trash2,
TrendingDown,
TrendingUp,
} from "lucide-react";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { useRouter, useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination";
import columns from "./columns";
import {
listDataAudio,
listDataImage,
listDataVideo,
listEnableCategory,
} from "@/service/content/content";
import { Label } from "@/components/ui/label";
import { format } from "date-fns";
import useTableColumns from "./columns";
const TableAudio = () => {
const router = useRouter();
const searchParams = useSearchParams();
const [dataTable, setDataTable] = React.useState<any[]>([]);
const [totalData, setTotalData] = 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 [showData, setShowData] = React.useState("10");
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: Number(showData),
});
const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1);
const [limit, setLimit] = React.useState(10);
const [search, setSearch] = React.useState<string>("");
const userId = getCookiesDecrypt("uie");
const userLevelId = getCookiesDecrypt("ulie");
const [categories, setCategories] = React.useState<any[]>([]);
const [selectedCategories, setSelectedCategories] = React.useState<number[]>(
[]
);
const [categoryFilter, setCategoryFilter] = React.useState<string>("");
const [statusFilter, setStatusFilter] = React.useState<any[]>([]);
const [startDate, setStartDate] = React.useState("");
const [endDate, setEndDate] = React.useState("");
const [filterByCreator, setFilterByCreator] = React.useState("");
const [filterBySource, setFilterBySource] = React.useState("");
const [filterByCreatorGroup, setFilterByCreatorGroup] = React.useState("");
const roleId = getCookiesDecrypt("urie");
const columns = useTableColumns();
const table = useReactTable({
data: dataTable,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
React.useEffect(() => {
fetchData();
getCategories();
}, [
categoryFilter,
statusFilter,
page,
showData,
search,
startDate,
endDate,
]);
async function getCategories() {
const category = await listEnableCategory("4");
const resCategory = category?.data?.data?.content;
setCategories(resCategory || []);
}
// Fungsi menangani perubahan checkbox
const handleCheckboxChange = (categoryId: number) => {
setSelectedCategories(
(prev: any) =>
prev.includes(categoryId)
? prev.filter((id: any) => id !== categoryId) // Hapus jika sudah dipilih
: [...prev, categoryId] // Tambahkan jika belum dipilih
);
// Perbarui filter kategori
setCategoryFilter((prev) => {
const updatedCategories = prev.split(",").filter(Boolean).map(Number);
const newCategories = updatedCategories.includes(categoryId)
? updatedCategories.filter((id) => id !== categoryId)
: [...updatedCategories, categoryId];
return newCategories.join(",");
});
};
async function fetchData() {
const formattedStartDate = startDate
? format(new Date(startDate), "yyyy-MM-dd")
: "";
const formattedEndDate = endDate
? format(new Date(endDate), "yyyy-MM-dd")
: "";
try {
const isForSelf = Number(roleId) === 4;
const res = await listDataAudio(
showData,
page - 1,
isForSelf,
!isForSelf,
categoryFilter,
statusFilter,
statusFilter?.sort().join(",").includes("1") ? userLevelId : "",
filterByCreator,
filterBySource,
formattedStartDate, // Pastikan format sesuai
formattedEndDate, // Pastikan format sesuai
search,
filterByCreatorGroup
);
const data = res?.data?.data;
const contentData = data?.content;
contentData.forEach((item: any, index: number) => {
item.no = (page - 1) * Number(showData) + index + 1;
});
setDataTable(contentData);
setTotalData(data?.totalElements);
setTotalPage(data?.totalPages);
} catch (error) {
console.error("Error fetching tasks:", error);
}
}
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value); // Perbarui state search
table.getColumn("judul")?.setFilterValue(e.target.value); // Set filter tabel
};
const handleSearchFilterBySource = (
e: React.ChangeEvent<HTMLInputElement>
) => {
const value = e.target.value;
setFilterBySource(value); // Perbarui state filter
fetchData(); // Panggil ulang data dengan filter baru
};
function handleStatusCheckboxChange(value: any) {
setStatusFilter((prev: any) =>
prev.includes(value)
? prev.filter((status: any) => status !== value)
: [...prev, value]
);
}
const handleSearchFilterByCreator = (
e: React.ChangeEvent<HTMLInputElement>
) => {
const value = e.target.value;
setFilterByCreator(value); // Perbarui state filter
fetchData(); // Panggil ulang data dengan filter baru
};
return (
<div className="w-full overflow-x-auto">
<div className="flex flex-col md:flex-row lg:flex-row md:justify-between lg:justify-between items-center md:px-5 lg:px-5">
<div className="relative w-full md:w-[200px] lg:w-[200px] px-2">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400 dark:text-white" />
<Input
type="text"
placeholder="Search Judul..."
className="pl-9 bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white"
value={search}
onChange={handleSearch}
/>
</div>
<div className="flex flex-row items-center gap-3">
<div className="flex items-center py-4">
<div className="mx-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="md" variant="outline">
{showData} Data
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 text-sm">
<DropdownMenuRadioGroup
value={showData}
onValueChange={setShowData}
>
<DropdownMenuRadioItem value="10">
10 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="50">
50 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="100">
100 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="250">
250 Data
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto" size="md">
Filter <ChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-64 h-[200px] overflow-y-auto"
>
<div className="flex flex-row justify-between my-1 mx-1">
<p>Filter</p>
{/* <p
className="text-blue-600 cursor-pointer"
onClick={fetchData}
>
Simpan
</p> */}
</div>
<Label className="ml-2">Kategori</Label>
{categories.length > 0 ? (
categories.map((category) => (
<div
key={category.id}
className="flex items-center px-4 py-1"
>
<input
type="checkbox"
id={`category-${category.id}`}
className="mr-2"
checked={selectedCategories.includes(category.id)}
onChange={() => handleCheckboxChange(category.id)}
/>
<label
htmlFor={`category-${category.id}`}
className="text-sm"
>
{category.name}
</label>
</div>
))
) : (
<p className="text-sm text-gray-500 px-4 py-2">
No categories found.
</p>
)}
<div className="mx-2 my-1">
<Label>Tanggal Awal</Label>
<Input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="max-w-sm"
/>
</div>
<div className="mx-2 my-1">
<Label>Tanggal Akhir</Label>
<Input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="max-w-sm"
/>
</div>
<div className="mx-2 my-1">
<Label>Kreator</Label>
<Input
placeholder="Filter Status..."
value={filterByCreator}
onChange={handleSearchFilterByCreator}
className="max-w-sm"
/>
</div>
<div className="mx-2 my-1">
<Label>Sumber</Label>
<Input
placeholder="Filter Status..."
value={filterBySource}
onChange={handleSearchFilterBySource}
className="max-w-sm"
/>
</div>
<Label className="ml-2 mt-2">Status</Label>
<div className="flex items-center px-4 py-1">
<input
type="checkbox"
id="status-2"
className="mr-2"
checked={statusFilter.includes(1)}
onChange={() => handleStatusCheckboxChange(1)}
/>
<label htmlFor="status-2" className="text-sm">
Menunggu Review
</label>
</div>
<div className="flex items-center px-4 py-1">
<input
type="checkbox"
id="status-2"
className="mr-2"
checked={statusFilter.includes(2)}
onChange={() => handleStatusCheckboxChange(2)}
/>
<label htmlFor="status-2" className="text-sm">
Diterima
</label>
</div>
<div className="flex items-center px-4 py-1">
<input
type="checkbox"
id="status-3"
className="mr-2"
checked={statusFilter.includes(3)}
onChange={() => handleStatusCheckboxChange(3)}
/>
<label htmlFor="status-3" className="text-sm">
Minta Update
</label>
</div>
<div className="flex items-center px-4 py-1">
<input
type="checkbox"
id="status-4"
className="mr-2"
checked={statusFilter.includes(4)}
onChange={() => handleStatusCheckboxChange(4)}
/>
<label htmlFor="status-4" className="text-sm">
Ditolak
</label>
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex items-center py-4">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto" size="md">
Columns <ChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
<Table className="overflow-hidden mt-3 mx-3">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-default-200">
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="h-[75px]"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<TablePagination
table={table}
totalData={totalData}
totalPage={totalPage}
/>
</div>
);
};
export default TableAudio;

View File

@ -0,0 +1,13 @@
import FormAudio from "@/components/form/content/audio/audio-form";
const AudioCreatePage = async () => {
return (
<div>
<div className="space-y-4">
<FormAudio />
</div>
</div>
);
};
export default AudioCreatePage;

View File

@ -0,0 +1,13 @@
import FormAudioDetail from "@/components/form/content/audio/audio-detail-form";
const AudioDetailPage = async () => {
return (
<div>
<div className="space-y-4">
<FormAudioDetail />
</div>
</div>
);
};
export default AudioDetailPage;

View File

@ -0,0 +1,11 @@
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Media Hub | POLRI",
description: "Media Hub merupakan situs resmi milik Divisi Humas Polri di mana di dalamnya berisi konten-konten yang dapat diakses secara gratis oleh Internal Polri, Jurnalis, Masyarakat Umum, dan KSP.",
};
const Layout = ({ children }: { children: React.ReactNode }) => {
return <>{children}</>;
};
export default Layout;

View File

@ -0,0 +1,46 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import { UploadIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Icon } from "@iconify/react/dist/iconify.js";
import TableAudio from "./components/table-audio";
import Link from "next/link";
const ReactTableAudioPage = () => {
return (
<div>
{/* <SiteBreadcrumb /> */}
<div className="space-y-4 m-3">
<Card>
<CardHeader className="border-b border-solid border-default-200 mb-6">
<CardTitle>
<div className="flex items-center">
<div className="flex-1 text-xl font-medium text-default-900">
Audio
</div>
<div className="flex-none">
<Link href={"/admin/content/audio/create"}>
<Button color="primary" className="text-white">
<UploadIcon size={18} className="mr-2" />
Create Audio
</Button>
</Link>
{/* <Button color="primary" className="text-white ml-3">
<UploadIcon />
Unggah Audio Dengan AI
</Button> */}
</div>
</div>
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<TableAudio />
</CardContent>
</Card>
</div>
</div>
);
};
export default ReactTableAudioPage;

View File

@ -0,0 +1,13 @@
import FormAudioUpdate from "@/components/form/content/audio/audio-update-form";
const AudioUpdatePage = async () => {
return (
<div>
<div className="space-y-4">
<FormAudioUpdate />
</div>
</div>
);
};
export default AudioUpdatePage;

View File

@ -0,0 +1,287 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { format } from "date-fns";
import { error } from "@/lib/swal";
import { deleteMedia } from "@/service/content/content";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
import Link from "next/link";
const useTableColumns = () => {
const MySwal = withReactContent(Swal);
const userLevelId = getCookiesDecrypt("ulie");
const columns: ColumnDef<any>[] = [
{
accessorKey: "no",
header: "No",
cell: ({ row }) => (
<div className="flex items-center gap-5">
<div className="flex-1 text-start">
<h4 className="text-sm font-medium text-default-600 whitespace-nowrap mb-1">
{row.getValue("no")}
</h4>
</div>
</div>
),
},
{
accessorKey: "title",
header: "Title",
cell: ({ row }: { row: { getValue: (key: string) => string } }) => {
const title: string = row.getValue("title");
return (
<span className="whitespace-nowrap">
{title.length > 50 ? `${title.slice(0, 30)}...` : title}
</span>
);
},
},
{
accessorKey: "categoryName",
header: "Category Name",
cell: ({ row }) => (
<span className="whitespace-nowrap">
{row.getValue("categoryName")}
</span>
),
},
{
accessorKey: "createdAt",
header: "Upload Date",
cell: ({ row }) => {
const createdAt = row.getValue("createdAt") as
| string
| number
| undefined;
const formattedDate =
createdAt && !isNaN(new Date(createdAt).getTime())
? format(new Date(createdAt), "dd-MM-yyyy HH:mm:ss")
: "-";
return <span className="whitespace-nowrap">{formattedDate}</span>;
},
},
{
accessorKey: "creatorName",
header: "Creator Group",
cell: ({ row }) => (
<span className="whitespace-nowrap">{row.getValue("creatorName")}</span>
),
},
{
accessorKey: "creatorGroupLevelName",
header: "Source",
cell: ({ row }) => (
<span className="whitespace-nowrap">
{row.getValue("creatorGroupLevelName")}
</span>
),
},
{
accessorKey: "publishedOn",
header: "Published",
cell: ({ row }) => {
const isPublish = row.original.isPublish;
const isPublishOnPolda = row.original.isPublishOnPolda;
const creatorGroupParentLevelId =
row.original.creatorGroupParentLevelId;
let displayText = "-";
if (isPublish && !isPublishOnPolda) {
displayText = "Mabes";
} else if (isPublish && isPublishOnPolda) {
if (Number(creatorGroupParentLevelId) == 761) {
displayText = "Mabes & Satker";
} else {
displayText = "Mabes & Polda";
}
} else if (!isPublish && isPublishOnPolda) {
if (Number(creatorGroupParentLevelId) == 761) {
displayText = "Satker";
} else {
displayText = "Polda";
}
}
return (
<div className="text-center whitespace-nowrap" title={displayText}>
{displayText}
</div>
);
},
},
{
accessorKey: "statusName",
header: "Status",
cell: ({ row }) => {
const statusColors: Record<string, string> = {
diterima: "bg-green-100 text-green-600",
"menunggu review": "bg-orange-100 text-orange-600",
};
const colors = [
"bg-orange-100 text-orange-600",
"bg-orange-100 text-orange-600",
"bg-green-100 text-green-600",
"bg-blue-100 text-blue-600",
"bg-red-200 text-red-600",
];
const status =
Number(row.original?.statusId) == 2 &&
row.original?.reviewedAtLevel !== null &&
!row.original?.reviewedAtLevel?.includes(`:${userLevelId}:`) &&
Number(row.original?.creatorGroupLevelId) != Number(userLevelId)
? "1"
: row.original?.statusId;
const statusStyles =
colors[Number(status)] || "bg-red-200 text-red-600";
// const statusStyles = statusColors[status] || "bg-red-200 text-red-600";
return (
<Badge
className={cn(
"rounded-full px-5 w-full whitespace-nowrap",
statusStyles
)}
>
{(Number(row.original?.statusId) == 2 &&
!row.original?.reviewedAtLevel !== null &&
!row.original?.reviewedAtLevel?.includes(
`:${Number(userLevelId)}:`
) &&
Number(row.original?.creatorGroupLevelId) !=
Number(userLevelId)) ||
(Number(row.original?.statusId) == 1 &&
Number(row.original?.needApprovalFromLevel) ==
Number(userLevelId))
? "Menunggu Review"
: row.original?.statusName}{" "}
</Badge>
);
},
},
{
id: "actions",
accessorKey: "action",
header: "Action",
enableHiding: false,
cell: ({ row }) => {
const MySwal = withReactContent(Swal);
async function doDelete(id: any) {
// loading();
const data = {
id,
};
const response = await deleteMedia(data);
if (response?.error) {
error(response.message);
return false;
}
success();
}
function success() {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
window.location.reload();
}
});
}
const handleDeleteMedia = (id: any) => {
MySwal.fire({
title: "Hapus Data",
text: "",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#3085d6",
confirmButtonColor: "#d33",
confirmButtonText: "Hapus",
}).then((result) => {
if (result.isConfirmed) {
doDelete(id);
}
});
};
const [isMabesApprover, setIsMabesApprover] = React.useState(false);
const userId = getCookiesDecrypt("uie");
const userLevelId = getCookiesDecrypt("ulie");
const roleId = getCookiesDecrypt("urie");
React.useEffect(() => {
if (userLevelId !== undefined && roleId !== undefined) {
setIsMabesApprover(
Number(userLevelId) == 216 && Number(roleId) == 3
);
}
}, [userLevelId, roleId]);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
>
<span className="sr-only">Open menu</span>
<MoreVertical className="h-4 w-4 text-default-800" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0" align="end">
<Link href={`/admin/content/document/detail/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group rounded-none">
<Eye className="w-4 h-4 me-1.5" />
View
</DropdownMenuItem>
</Link>
{(Number(row.original.uploadedById) === Number(userId) ||
isMabesApprover) && (
<Link
href={`/admin/content/document/update/${row.original.id}`}
>
<DropdownMenuItem className="p-2 border-b text-default-700 group rounded-none">
<SquarePen className="w-4 h-4 me-1.5" />
Edit
</DropdownMenuItem>
</Link>
)}
<DropdownMenuItem
onClick={() => handleDeleteMedia(row.original.id)}
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-white rounded-none"
>
<Trash2 className="w-4 h-4 me-1.5 focus:text-white" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
return columns;
};
export default useTableColumns;

View File

@ -0,0 +1,496 @@
"use client";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
ChevronDown,
ChevronLeft,
ChevronRight,
Eye,
MoreVertical,
Search,
SquarePen,
Trash2,
TrendingDown,
TrendingUp,
} from "lucide-react";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { useRouter, useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination";
import columns from "./columns";
import {
listDataImage,
listDataTeks,
listEnableCategory,
} from "@/service/content/content";
import { Label } from "@/components/ui/label";
import { format } from "date-fns";
import useTableColumns from "./columns";
const TableTeks = () => {
const router = useRouter();
const searchParams = useSearchParams();
const [dataTable, setDataTable] = React.useState<any[]>([]);
const [totalData, setTotalData] = 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 [showData, setShowData] = React.useState("50");
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: Number(showData),
});
const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1);
const [limit, setLimit] = React.useState(10);
const [search, setSearch] = React.useState<string>("");
const userId = getCookiesDecrypt("uie");
const userLevelId = getCookiesDecrypt("ulie");
const [categories, setCategories] = React.useState<any[]>([]);
const [selectedCategories, setSelectedCategories] = React.useState<number[]>(
[]
);
const [categoryFilter, setCategoryFilter] = React.useState<string>("");
const [statusFilter, setStatusFilter] = React.useState<any[]>([]);
const [startDate, setStartDate] = React.useState("");
const [endDate, setEndDate] = React.useState("");
const [filterByCreator, setFilterByCreator] = React.useState("");
const [filterBySource, setFilterBySource] = React.useState("");
const [filterByCreatorGroup, setFilterByCreatorGroup] = React.useState("");
const roleId = getCookiesDecrypt("urie");
const columns = useTableColumns();
const table = useReactTable({
data: dataTable,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
React.useEffect(() => {
fetchData();
getCategories();
}, [
categoryFilter,
statusFilter,
page,
showData,
search,
startDate,
endDate,
]);
async function getCategories() {
const category = await listEnableCategory("3");
const resCategory = category?.data?.data?.content;
setCategories(resCategory || []);
}
// Fungsi menangani perubahan checkbox
const handleCheckboxChange = (categoryId: number) => {
setSelectedCategories(
(prev: any) =>
prev.includes(categoryId)
? prev.filter((id: any) => id !== categoryId) // Hapus jika sudah dipilih
: [...prev, categoryId] // Tambahkan jika belum dipilih
);
// Perbarui filter kategori
setCategoryFilter((prev) => {
const updatedCategories = prev.split(",").filter(Boolean).map(Number);
const newCategories = updatedCategories.includes(categoryId)
? updatedCategories.filter((id) => id !== categoryId)
: [...updatedCategories, categoryId];
return newCategories.join(",");
});
};
async function fetchData() {
const formattedStartDate = startDate
? format(new Date(startDate), "yyyy-MM-dd")
: "";
const formattedEndDate = endDate
? format(new Date(endDate), "yyyy-MM-dd")
: "";
try {
const isForSelf = Number(roleId) === 4;
const res = await listDataTeks(
showData,
page - 1,
isForSelf,
!isForSelf,
categoryFilter,
statusFilter,
statusFilter?.sort().join(",").includes("1") ? userLevelId : "",
filterByCreator,
filterBySource,
formattedStartDate, // Pastikan format sesuai
formattedEndDate, // Pastikan format sesuai
search,
filterByCreatorGroup
);
const data = res?.data?.data;
const contentData = data?.content;
contentData.forEach((item: any, index: number) => {
item.no = (page - 1) * Number(showData) + index + 1;
});
setDataTable(contentData);
setTotalData(data?.totalElements);
setTotalPage(data?.totalPages);
} catch (error) {
console.error("Error fetching tasks:", error);
}
}
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value); // Perbarui state search
table.getColumn("judul")?.setFilterValue(e.target.value); // Set filter tabel
};
const handleSearchFilterBySource = (
e: React.ChangeEvent<HTMLInputElement>
) => {
const value = e.target.value;
setFilterBySource(value); // Perbarui state filter
fetchData(); // Panggil ulang data dengan filter baru
};
function handleStatusCheckboxChange(value: any) {
setStatusFilter((prev: any) =>
prev.includes(value)
? prev.filter((status: any) => status !== value)
: [...prev, value]
);
}
const handleSearchFilterByCreator = (
e: React.ChangeEvent<HTMLInputElement>
) => {
const value = e.target.value;
setFilterByCreator(value); // Perbarui state filter
fetchData(); // Panggil ulang data dengan filter baru
};
return (
<div className="w-full overflow-x-auto">
<div className="flex flex-col md:flex-row lg:flex-row md:justify-between lg:justify-between items-center md:px-5 lg:px-5">
<div className="relative w-full md:w-[200px] lg:w-[200px]">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400 dark:text-white" />
<Input
type="text"
placeholder="Search Judul..."
className="pl-9 bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white"
value={search}
onChange={handleSearch}
/>
</div>
<div className="flex flex-row items-center gap-3">
<div className="flex items-center py-4">
<div className="mx-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="md" variant="outline">
{showData} Data
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 text-sm">
<DropdownMenuRadioGroup
value={showData}
onValueChange={setShowData}
>
<DropdownMenuRadioItem value="10">
10 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="50">
50 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="100">
100 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="250">
250 Data
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto" size="md">
Filter <ChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-64 h-[200px] overflow-y-auto"
>
<div className="flex flex-row justify-between my-1 mx-1">
<p>Filter</p>
{/* <p
className="text-blue-600 cursor-pointer"
onClick={fetchData}
>
Simpan
</p> */}
</div>
<Label className="ml-2">Kategori</Label>
{categories.length > 0 ? (
categories.map((category) => (
<div
key={category.id}
className="flex items-center px-4 py-1"
>
<input
type="checkbox"
id={`category-${category.id}`}
className="mr-2"
checked={selectedCategories.includes(category.id)}
onChange={() => handleCheckboxChange(category.id)}
/>
<label
htmlFor={`category-${category.id}`}
className="text-sm"
>
{category.name}
</label>
</div>
))
) : (
<p className="text-sm text-gray-500 px-4 py-2">
No categories found.
</p>
)}
<div className="mx-2 my-1">
<Label>Tanggal Awal</Label>
<Input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="max-w-sm"
/>
</div>
<div className="mx-2 my-1">
<Label>Tanggal Akhir</Label>
<Input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="max-w-sm"
/>
</div>
<div className="mx-2 my-1">
<Label>Kreator</Label>
<Input
placeholder="Filter Status..."
value={filterByCreator}
onChange={handleSearchFilterByCreator}
className="max-w-sm"
/>
</div>
<div className="mx-2 my-1">
<Label>Sumber</Label>
<Input
placeholder="Filter Status..."
value={filterBySource}
onChange={handleSearchFilterBySource}
className="max-w-sm"
/>
</div>
<Label className="ml-2 mt-2">Status</Label>
<div className="flex items-center px-4 py-1">
<input
type="checkbox"
id="status-2"
className="mr-2"
checked={statusFilter.includes(1)}
onChange={() => handleStatusCheckboxChange(1)}
/>
<label htmlFor="status-2" className="text-sm">
Menunggu Review
</label>
</div>
<div className="flex items-center px-4 py-1">
<input
type="checkbox"
id="status-2"
className="mr-2"
checked={statusFilter.includes(2)}
onChange={() => handleStatusCheckboxChange(2)}
/>
<label htmlFor="status-2" className="text-sm">
Diterima
</label>
</div>
<div className="flex items-center px-4 py-1">
<input
type="checkbox"
id="status-3"
className="mr-2"
checked={statusFilter.includes(3)}
onChange={() => handleStatusCheckboxChange(3)}
/>
<label htmlFor="status-3" className="text-sm">
Minta Update
</label>
</div>
<div className="flex items-center px-4 py-1">
<input
type="checkbox"
id="status-4"
className="mr-2"
checked={statusFilter.includes(4)}
onChange={() => handleStatusCheckboxChange(4)}
/>
<label htmlFor="status-4" className="text-sm">
Ditolak
</label>
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex items-center py-4">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto" size="md">
Columns <ChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
<Table className="overflow-hidden mt-3 mx-3">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-default-200">
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="h-[75px]"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<TablePagination
table={table}
totalData={totalData}
totalPage={totalPage}
/>
</div>
);
};
export default TableTeks;

View File

@ -0,0 +1,14 @@
import FormTeks from "@/components/form/content/document/teks-form";
const TeksCreatePage = async () => {
return (
<div>
{/* <SiteBreadcrumb /> */}
<div className="space-y-4">
<FormTeks />
</div>
</div>
);
};
export default TeksCreatePage;

View File

@ -0,0 +1,14 @@
import FormTeksDetail from "@/components/form/content/document/teks-detail-form";
const TeksDetailPage = async () => {
return (
<div>
{/* <SiteBreadcrumb /> */}
<div className="space-y-4">
<FormTeksDetail />
</div>
</div>
);
};
export default TeksDetailPage;

View File

@ -0,0 +1,11 @@
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Media Hub | POLRI",
description: "Media Hub merupakan situs resmi milik Divisi Humas Polri di mana di dalamnya berisi konten-konten yang dapat diakses secara gratis oleh Internal Polri, Jurnalis, Masyarakat Umum, dan KSP.",
};
const Layout = ({ children }: { children: React.ReactNode }) => {
return <>{children}</>;
};
export default Layout;

View File

@ -0,0 +1,40 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { UploadIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import TableTeks from "./components/table-teks";
import Link from "next/link";
const ReactTableTeksPage = () => {
return (
<div>
<div className="space-y-4 m-3">
<Card>
<CardHeader className="border-b border-solid border-default-200 mb-6">
<CardTitle>
<div className="flex items-center">
<div className="flex-1 text-xl font-medium text-default-900">
Text
</div>
<div className="flex-none">
<Link href={"/admin/content/document/create"}>
<Button color="primary" className="text-white">
<UploadIcon size={18} className="mr-2" />
Create Text
</Button>
</Link>
</div>
</div>
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<TableTeks />
</CardContent>
</Card>
</div>
</div>
);
};
export default ReactTableTeksPage;

View File

@ -0,0 +1,13 @@
import FormTeksUpdate from "@/components/form/content/document/teks-update-form";
const TeksUpdatePage = async () => {
return (
<div>
<div className="space-y-4">
<FormTeksUpdate />
</div>
</div>
);
};
export default TeksUpdatePage;

View File

@ -0,0 +1,306 @@
"use client";
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { format } from "date-fns";
import { useRouter } from "next/navigation";
import { deleteMedia } from "@/service/content/content";
import { error, loading } from "@/lib/swal";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import Link from "next/link";
const useTableColumns = () => {
const MySwal = withReactContent(Swal);
const userLevelId = getCookiesDecrypt("ulie");
const columns: ColumnDef<any>[] = [
{
accessorKey: "no",
header: "No",
cell: ({ row }) => (
<div className="flex items-center gap-5">
<div className="flex-1 text-start">
<h4 className="text-sm font-medium text-default-600 whitespace-nowrap mb-1">
{row.getValue("no")}
</h4>
</div>
</div>
),
},
{
accessorKey: "title",
header: "Title",
cell: ({ row }: { row: { getValue: (key: string) => string } }) => {
const title: string = row.getValue("title");
return (
<span className="whitespace-nowrap">
{title.length > 50 ? `${title.slice(0, 30)}...` : title}
</span>
);
},
},
{
accessorKey: "categoryName",
header: "Category Name",
cell: ({ row }) => (
<span className="whitespace-nowrap">
{row.getValue("categoryName")}
</span>
),
},
{
accessorKey: "createdAt",
header: "Upload Date",
cell: ({ row }) => {
const createdAt = row.getValue("createdAt") as
| string
| number
| undefined;
const formattedDate =
createdAt && !isNaN(new Date(createdAt).getTime())
? format(new Date(createdAt), "dd-MM-yyyy HH:mm:ss")
: "-";
return <span className="whitespace-nowrap">{formattedDate}</span>;
},
},
{
accessorKey: "creatorName",
header: "Creator Group",
cell: ({ row }) => (
<span className="whitespace-nowrap">{row.getValue("creatorName")}</span>
),
},
{
accessorKey: "creatorGroupLevelName",
header: "Source",
cell: ({ row }) => (
<span className="whitespace-nowrap">
{row.getValue("creatorGroupLevelName")}
</span>
),
},
{
accessorKey: "publishedOn",
header: "Published",
cell: ({ row }) => {
const isPublish = row.original.isPublish;
const isPublishOnPolda = row.original.isPublishOnPolda;
const creatorGroupParentLevelId =
row.original.creatorGroupParentLevelId;
let displayText = "-";
if (isPublish && !isPublishOnPolda) {
displayText = "Mabes";
} else if (isPublish && isPublishOnPolda) {
if (Number(creatorGroupParentLevelId) == 761) {
displayText = "Mabes & Satker";
} else {
displayText = "Mabes & Polda";
}
} else if (!isPublish && isPublishOnPolda) {
if (Number(creatorGroupParentLevelId) == 761) {
displayText = "Satker";
} else {
displayText = "Polda";
}
}
return (
<div className="text-center whitespace-nowrap" title={displayText}>
{displayText}
</div>
);
},
},
//
{
accessorKey: "statusName",
header: "Status",
cell: ({ row }) => {
const statusId = Number(row.original?.statusId);
const reviewedAtLevel = row.original?.reviewedAtLevel || "";
const creatorGroupLevelId = Number(row.original?.creatorGroupLevelId);
const needApprovalFromLevel = Number(
row.original?.needApprovalFromLevel
);
const userHasReviewed = reviewedAtLevel.includes(`:${userLevelId}:`);
const isCreator = creatorGroupLevelId === Number(userLevelId);
const isWaitingForReview =
statusId === 2 && !userHasReviewed && !isCreator;
const isApprovalNeeded =
statusId === 1 && needApprovalFromLevel === Number(userLevelId);
const label =
isWaitingForReview || isApprovalNeeded
? "Menunggu Review"
: statusId === 2
? "Diterima"
: row.original?.statusName;
const colors: Record<string, string> = {
"Menunggu Review": "bg-orange-100 text-orange-600",
Diterima: "bg-green-100 text-green-600",
default: "bg-red-200 text-red-600",
};
const statusStyles = colors[label] || colors.default;
return (
<Badge
className={cn(
"rounded-full px-5 w-full whitespace-nowrap",
statusStyles
)}
>
{label}
</Badge>
);
},
},
{
id: "actions",
accessorKey: "action",
header: "Action",
enableHiding: false,
cell: ({ row }) => {
const router = useRouter();
const MySwal = withReactContent(Swal);
async function doDelete(id: any) {
// loading();
const data = {
id,
};
const response = await deleteMedia(data);
if (response?.error) {
error(response.message);
return false;
}
success();
}
function success() {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
window.location.reload();
}
});
}
const handleDeleteMedia = (id: any) => {
MySwal.fire({
title: "Hapus Data",
text: "",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#3085d6",
confirmButtonColor: "#d33",
confirmButtonText: "Hapus",
}).then((result) => {
if (result.isConfirmed) {
doDelete(id);
}
});
};
const [isMabesApprover, setIsMabesApprover] = React.useState(false);
const userId = getCookiesDecrypt("uie");
const userLevelId = getCookiesDecrypt("ulie");
const roleId = getCookiesDecrypt("urie");
React.useEffect(() => {
if (userLevelId !== undefined && roleId !== undefined) {
setIsMabesApprover(
Number(userLevelId) == 216 && Number(roleId) == 3
);
}
}, [userLevelId, roleId]);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
>
<span className="sr-only">Open menu</span>
<MoreVertical className="h-4 w-4 text-default-800" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0 hover:text-black" align="end">
<Link
href={`/admin/content/image/detail/${row.original.id}`}
className="hover:text-black"
>
<DropdownMenuItem className="p-2 border-b text-default-700 group rounded-none">
<Eye className="w-4 h-4 me-1.5" />
View
</DropdownMenuItem>
</Link>
{/* <Link
href={`/contributor/content/image/update/${row.original.id}`}
>
<DropdownMenuItem className="p-2 border-b text-default-700 group rounded-none">
<SquarePen className="w-4 h-4 me-1.5" />
Edit
</DropdownMenuItem>
</Link> */}
{(Number(row.original.uploadedById) === Number(userId) ||
isMabesApprover) && (
<Link href={`/admin/content/image/update/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group rounded-none">
<SquarePen className="w-4 h-4 me-1.5" />
Edit
</DropdownMenuItem>
</Link>
)}
<DropdownMenuItem
onClick={() => handleDeleteMedia(row.original.id)}
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-white rounded-none"
>
<Trash2 className="w-4 h-4 me-1.5 focus:text-white" />
Delete
</DropdownMenuItem>
{/* {(row.original.uploadedById === userId || isMabesApprover) && (
<DropdownMenuItem
onClick={() => handleDeleteMedia(row.original.id)}
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-destructive-foreground rounded-none"
>
<Trash2 className="w-4 h-4 me-1.5" />
Hapus
</DropdownMenuItem>
)} */}
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
return columns;
};
export default useTableColumns;

View File

@ -0,0 +1,505 @@
"use client";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
ChevronDown,
ChevronLeft,
ChevronRight,
Eye,
MoreVertical,
Search,
SquarePen,
Trash2,
TrendingDown,
TrendingUp,
} from "lucide-react";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination";
import {
deleteMedia,
listDataImage,
listEnableCategory,
} from "@/service/content/content";
import { loading } from "@/config/swal";
import { toast } from "sonner";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { error } from "@/lib/swal";
import { Label } from "@/components/ui/label";
import { format } from "date-fns";
import useTableColumns from "./columns";
const TableImage = () => {
const router = useRouter();
const searchParams = useSearchParams();
const params = useParams();
const locale = params?.locale;
const MySwal = withReactContent(Swal);
const [dataTable, setDataTable] = React.useState<any[]>([]);
const [totalData, setTotalData] = 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 [showData, setShowData] = React.useState("50");
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: Number(showData),
});
const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1);
const [search, setSearch] = React.useState("");
const userId = getCookiesDecrypt("uie");
const userLevelId = getCookiesDecrypt("ulie");
const [categories, setCategories] = React.useState<any[]>([]);
const [selectedCategories, setSelectedCategories] = React.useState<number[]>(
[]
);
const [categoryFilter, setCategoryFilter] = React.useState<string>("");
const [statusFilter, setStatusFilter] = React.useState<any[]>([]);
const [startDate, setStartDate] = React.useState("");
const [endDate, setEndDate] = React.useState("");
const [filterByCreator, setFilterByCreator] = React.useState("");
const [filterBySource, setFilterBySource] = React.useState("");
const [filterByCreatorGroup, setFilterByCreatorGroup] = React.useState("");
const roleId = getCookiesDecrypt("urie");
const columns = useTableColumns();
const table = useReactTable({
data: dataTable,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
React.useEffect(() => {
fetchData();
getCategories();
}, [
categoryFilter,
statusFilter,
page,
showData,
search,
startDate,
endDate,
]);
async function getCategories() {
const category = await listEnableCategory("1");
const resCategory = category?.data?.data?.content;
setCategories(resCategory || []);
}
// Fungsi menangani perubahan checkbox
const handleCheckboxChange = (categoryId: number) => {
setSelectedCategories((prev: any) =>
prev.includes(categoryId)
? prev.filter((id: any) => id !== categoryId)
: [...prev, categoryId]
);
// Perbarui filter kategori
setCategoryFilter((prev) => {
const updatedCategories = prev.split(",").filter(Boolean).map(Number);
const newCategories = updatedCategories.includes(categoryId)
? updatedCategories.filter((id) => id !== categoryId)
: [...updatedCategories, categoryId];
return newCategories.join(",");
});
};
async function fetchData() {
const formattedStartDate = startDate
? format(new Date(startDate), "yyyy-MM-dd")
: "";
const formattedEndDate = endDate
? format(new Date(endDate), "yyyy-MM-dd")
: "";
try {
const isForSelf = Number(roleId) === 4;
const res = await listDataImage(
showData,
page - 1,
isForSelf,
!isForSelf,
categoryFilter,
statusFilter,
statusFilter?.sort().join(",").includes("1") ? userLevelId : "",
filterByCreator,
filterBySource,
formattedStartDate,
formattedEndDate,
search,
filterByCreatorGroup,
locale == "en"
);
const data = res?.data?.data;
const contentData = data?.content;
contentData.forEach((item: any, index: number) => {
item.no = (page - 1) * Number(showData) + index + 1;
});
setDataTable(contentData);
setTotalData(data?.totalElements);
setTotalPage(data?.totalPages);
} catch (error) {
console.error("Error fetching tasks:", error);
}
}
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
table.getColumn("judul")?.setFilterValue(e.target.value);
};
const handleSearchFilterBySource = (
e: React.ChangeEvent<HTMLInputElement>
) => {
const value = e.target.value;
setFilterBySource(value);
fetchData();
};
function handleStatusCheckboxChange(value: any) {
setStatusFilter((prev: any) =>
prev.includes(value)
? prev.filter((status: any) => status !== value)
: [...prev, value]
);
}
const handleSearchFilterByCreator = (
e: React.ChangeEvent<HTMLInputElement>
) => {
const value = e.target.value;
setFilterByCreator(value);
fetchData();
};
return (
<div className="w-full overflow-x-auto">
<div className="flex flex-col md:flex-row lg:flex-row md:justify-between lg:justify-between items-center md:px-5 lg:px-5">
<div className="relative w-full md:w-[200px] lg:w-[200px] px-2">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400 dark:text-white" />
<Input
type="text"
placeholder="Search Judul..."
className="pl-9 bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white"
value={search}
onChange={handleSearch}
/>
</div>
<div className="flex flex-row items-center gap-3">
<div className="flex items-center py-4">
<div className="mx-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="md" variant="outline">
{showData} Data
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 text-sm">
<DropdownMenuRadioGroup
value={showData}
onValueChange={setShowData}
>
<DropdownMenuRadioItem value="10">
10 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="50">
50 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="100">
100 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="250">
250 Data
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto" size="md">
Filter <ChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-64 h-[200px] overflow-y-auto"
>
<div className="flex flex-row justify-between my-1 mx-1">
<p>Filter</p>
{/* <p
className="text-blue-600 cursor-pointer"
onClick={fetchData}
>
Simpan
</p> */}
</div>
<Label className="ml-2">Kategori</Label>
{categories.length > 0 ? (
categories.map((category) => (
<div
key={category.id}
className="flex items-center px-4 py-1"
>
<input
type="checkbox"
id={`category-${category.id}`}
className="mr-2"
checked={selectedCategories.includes(category.id)}
onChange={() => handleCheckboxChange(category.id)}
/>
<label
htmlFor={`category-${category.id}`}
className="text-sm"
>
{category.name}
</label>
</div>
))
) : (
<p className="text-sm text-gray-500 px-4 py-2">
No categories found.
</p>
)}
<div className="mx-2 my-1">
<Label>Tanggal Awal</Label>
<Input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="max-w-sm"
/>
</div>
<div className="mx-2 my-1">
<Label>Tanggal Akhir</Label>
<Input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="max-w-sm"
/>
</div>
<div className="mx-2 my-1">
<Label>Kreator</Label>
<Input
placeholder="Filter Status..."
value={filterByCreator}
onChange={handleSearchFilterByCreator}
className="max-w-sm"
/>
</div>
<div className="mx-2 my-1">
<Label>Sumber</Label>
<Input
placeholder="Filter Status..."
value={filterBySource}
onChange={handleSearchFilterBySource}
className="max-w-sm"
/>
</div>
<Label className="ml-2 mt-2">Status</Label>
<div className="flex items-center px-4 py-1">
<input
type="checkbox"
id="status-2"
className="mr-2"
checked={statusFilter.includes(1)}
onChange={() => handleStatusCheckboxChange(1)}
/>
<label htmlFor="status-2" className="text-sm">
Menunggu Review
</label>
</div>
<div className="flex items-center px-4 py-1">
<input
type="checkbox"
id="status-2"
className="mr-2"
checked={statusFilter.includes(2)}
onChange={() => handleStatusCheckboxChange(2)}
/>
<label htmlFor="status-2" className="text-sm">
Diterima
</label>
</div>
<div className="flex items-center px-4 py-1">
<input
type="checkbox"
id="status-3"
className="mr-2"
checked={statusFilter.includes(3)}
onChange={() => handleStatusCheckboxChange(3)}
/>
<label htmlFor="status-3" className="text-sm">
Minta Update
</label>
</div>
<div className="flex items-center px-4 py-1">
<input
type="checkbox"
id="status-4"
className="mr-2"
checked={statusFilter.includes(4)}
onChange={() => handleStatusCheckboxChange(4)}
/>
<label htmlFor="status-4" className="text-sm">
Ditolak
</label>
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex items-center py-4">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto" size="md">
Columns <ChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
<Table className="overflow-hidden mt-3 mx-3">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-default-200">
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="h-[75px]"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<TablePagination
table={table}
totalData={totalData}
totalPage={totalPage}
/>
</div>
);
};
export default TableImage;

View File

@ -0,0 +1,15 @@
import FormImage from "@/components/form/content/image/image-form";
import SiteBreadcrumb from "@/components/site-breadcrumb";
const ImageCreatePage = async () => {
return (
<div>
{/* <SiteBreadcrumb /> */}
<div className="space-y-4">
<FormImage />
</div>
</div>
);
};
export default ImageCreatePage;

View File

@ -0,0 +1,14 @@
import FormImageDetail from "@/components/form/content/image/image-detail-form";
const ImageDetailPage = async () => {
return (
<div>
{/* <SiteBreadcrumb /> */}
<div className="space-y-4">
<FormImageDetail />
</div>
</div>
);
};
export default ImageDetailPage;

View File

@ -0,0 +1,11 @@
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Media Hub | POLRI",
description: "Media Hub merupakan situs resmi milik Divisi Humas Polri di mana di dalamnya berisi konten-konten yang dapat diakses secara gratis oleh Internal Polri, Jurnalis, Masyarakat Umum, dan KSP.",
};
const Layout = ({ children }: { children: React.ReactNode }) => {
return <>{children}</>;
};
export default Layout;

View File

@ -0,0 +1,27 @@
"use client";
import ImageTable from "@/components/table/image-table";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { ChevronDown, Upload } from "lucide-react";
export default function ImagePage() {
return (
<div className="overflow-x-hidden w-full">
<div className="px-2 md:px-4 md:py-4 w-full">
{/* Card utama */}
<div className="bg-white shadow-lg dark:bg-[#18181b] rounded-xl p-3">
<div className="flex justify-between items-center mb-3">
<h2 className="text-xl font-semibold">Image</h2>
<Button className="bg-[#006CFF] text-white hover:bg-[#0050c4] flex items-center">
<Upload className="mr-2 h-4 w-4" />
Unggah Foto
</Button>
</div>
<ImageTable />
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,47 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import TableImage from "./components/table-image";
import { UploadIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import Link from "next/link";
const ReactTableImagePage = () => {
return (
<div>
{/* <SiteBreadcrumb /> */}
<div className="space-y-4 m-3">
<Card>
<CardHeader className="border-b border-solid border-default-200 mb-6">
<CardTitle>
<div className="flex items-center">
<div className="flex-1 text-xl font-medium text-default-900">
Image
</div>
<div className="flex-none">
<Link href={"/admin/content/image/create"}>
<Button color="primary" className="text-white">
<UploadIcon size={18} className="mr-2" />
Create Image
</Button>
</Link>
{/* <Link href={"/contributor/content/image/createAi"}>
<Button color="primary" className="text-white ml-3">
<UploadIcon />
Unggah Foto Dengan AI
</Button>
</Link> */}
</div>
</div>
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<TableImage />
</CardContent>
</Card>
</div>
</div>
);
};
export default ReactTableImagePage;

View File

@ -0,0 +1,13 @@
import FormImageUpdate from "@/components/form/content/image/image-update-form";
const ImageUpdatePage = async () => {
return (
<div>
<div className="space-y-4">
<FormImageUpdate />
</div>
</div>
);
};
export default ImageUpdatePage;

View File

@ -0,0 +1,34 @@
"use client";
import DashboardContainer from "@/components/main/dashboard/dashboard-container";
import { motion } from "framer-motion";
import { useEffect, useState } from "react";
export default function AdminPage() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return (
<div className="h-full overflow-auto bg-gradient-to-br from-slate-50/50 via-white to-slate-50/50 flex items-center justify-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500"></div>
</div>
);
}
return (
<motion.div
className="h-full overflow-auto bg-gradient-to-br from-slate-50/50 via-white to-slate-50/50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
<div className="p-6">
<DashboardContainer />
</div>
</motion.div>
);
}

View File

@ -0,0 +1,11 @@
"use client";
import { AdminLayout } from "@/components/layout/admin-layout";
export default function AdminPageLayout({
children,
}: {
children: React.ReactNode;
}) {
return <AdminLayout>{children}</AdminLayout>;
}

16
app/auth/layout.tsx Normal file
View File

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

6
app/auth/page-1.tsx Normal file
View File

@ -0,0 +1,6 @@
import Login from "@/components/form/login";
import React from "react";
export default function AuthPage() {
return <>{/* <Login /> */}</>;
}

122
app/auth/page.tsx Normal file
View File

@ -0,0 +1,122 @@
"use client";
import React, { useState } from "react";
import { AuthLayout } from "@/components/auth/auth-layout";
import { LoginForm } from "@/components/auth/login-form";
import { EmailSetupForm } from "@/components/auth/email-setup-form";
import { OTPForm } from "@/components/auth/otp-form";
import { LoginFormData } from "@/types/auth";
import { useAuth, useEmailValidation } from "@/hooks/use-auth";
import { toast } from "sonner";
type AuthStep = "login" | "email-setup" | "otp";
const AuthPage = ({ params: { locale } }: { params: { locale: string } }) => {
const [currentStep, setCurrentStep] = useState<AuthStep>("login");
const [loginCredentials, setLoginCredentials] =
useState<LoginFormData | null>(null);
const { validateEmail } = useEmailValidation();
const { login } = useAuth();
const handleLoginSuccess = async (data: LoginFormData) => {
setLoginCredentials(data);
// Check email validation to determine next step
try {
const result = await validateEmail(data);
switch (result) {
case "skip":
handleOTPSuccess();
break;
case "setup":
setCurrentStep("email-setup");
break;
case "otp":
setCurrentStep("otp");
break;
case "success":
// The login hook will handle navigation automatically
break;
default:
toast.error("Unexpected response from email validation");
}
} catch (error: any) {
toast.error(error.message || "Email validation failed");
}
};
const handleLoginError = (error: string) => {
toast.error(error);
};
const handleEmailSetupSuccess = () => {
setCurrentStep("otp");
};
const handleEmailSetupError = (error: string) => {
toast.error(error);
};
const handleEmailSetupBack = () => {
setCurrentStep("login");
};
const handleOTPSuccess = async () => {
if (loginCredentials) {
try {
await login(loginCredentials);
// Navigation handled by login
} catch (error: any) {
toast.error(error.message || "Login failed after OTP verification");
}
}
};
const handleOTPError = (error: string) => {
toast.error(error);
};
const handleOTPResend = () => {
toast.info("OTP resent successfully");
};
const renderCurrentStep = () => {
switch (currentStep) {
case "login":
return (
<LoginForm
onSuccess={handleLoginSuccess}
onError={handleLoginError}
/>
);
case "email-setup":
return (
<EmailSetupForm
loginCredentials={loginCredentials}
onSuccess={handleEmailSetupSuccess}
onError={handleEmailSetupError}
onBack={handleEmailSetupBack}
/>
);
case "otp":
return (
<OTPForm
loginCredentials={loginCredentials}
onSuccess={handleOTPSuccess}
onError={handleOTPError}
onResend={handleOTPResend}
/>
);
default:
return (
<LoginForm
onSuccess={handleLoginSuccess}
onError={handleLoginError}
/>
);
}
};
return <AuthLayout>{renderCurrentStep()}</AuthLayout>;
};
export default AuthPage;

View File

@ -0,0 +1,10 @@
import SignUp from "@/components/form/sign-up";
import React from "react";
export default function AuthSignUpPage() {
return (
<>
<SignUp />
</>
);
}

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

122
app/globals.css Normal file
View File

@ -0,0 +1,122 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

30
app/layout.tsx Normal file
View File

@ -0,0 +1,30 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
// const geistSans = Geist({
// variable: "--font-geist-sans",
// subsets: ["latin"],
// });
// const geistMono = Geist_Mono({
// variable: "--font-geist-mono",
// subsets: ["latin"],
// });
export const metadata: Metadata = {
title: "NetidHub",
description: "NetidHub",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}

21
app/page.tsx Normal file
View File

@ -0,0 +1,21 @@
import Category from "@/components/landing-page/category";
import Footer from "@/components/landing-page/footer";
import Header from "@/components/landing-page/header";
import MediaUpdate from "@/components/landing-page/media-update";
import Navbar from "@/components/landing-page/navbar";
export default function Home() {
return (
<div className="relative min-h-screen font-[family-name:var(--font-geist-sans)]">
<div className="relative z-10 bg-white w-full mx-auto">
<Navbar />
<div className="flex-1">
<Header />
</div>
<MediaUpdate />
<Category />
<Footer />
</div>
</div>
);
}

View File

@ -0,0 +1,10 @@
import AudioDetail from "@/components/main/content/audio-detail";
import DetailVideo from "@/components/main/content/video-detail";
interface DetailInfoProps {
params: { id: string };
}
export default async function DetailInfo({ params }: DetailInfoProps) {
return <AudioDetail id={params.id} />;
}

View File

@ -0,0 +1,9 @@
import ImageDetail from "@/components/main/content/image-detail";
interface DetailInfoProps {
params: { id: string };
}
export default async function DetailImageInfo({ params }: DetailInfoProps) {
return <ImageDetail id={params?.id} />;
}

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

@ -0,0 +1,9 @@
import DocumentDetail from "@/components/main/content/document-detail";
interface DetailInfoProps {
params: { id: string };
}
export default async function DetailInfo({ params }: DetailInfoProps) {
return <DocumentDetail id={params.id} />;
}

View File

@ -0,0 +1,5 @@
import DetailCommentVideo from "@/components/main/comment-detail-video";
export default async function DetailCommentInfo() {
return <DetailCommentVideo />;
}

View File

@ -0,0 +1,9 @@
import DetailVideo from "@/components/main/content/video-detail";
interface DetailInfoProps {
params: { id: string };
}
export default async function DetailInfo({ params }: DetailInfoProps) {
return <DetailVideo id={params.id} />;
}

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

@ -0,0 +1,9 @@
import PublicationForYouLayout from "@/components/main/filter-publication-for-you";
export default async function FilterForYou() {
return (
<>
<PublicationForYouLayout />
</>
);
}

View File

@ -0,0 +1,9 @@
import PublicationBumnLayout from "@/components/main/filter-publication-bumn";
export default async function FilterBumn() {
return (
<>
<PublicationBumnLayout />
</>
);
}

View File

@ -0,0 +1,9 @@
import PublicationKlLayout from "@/components/main/filter-publication-kl";
export default async function FilterKl() {
return (
<>
<PublicationKlLayout />
</>
);
}

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

@ -0,0 +1,9 @@
import PublicationPemerintahDaerahLayout from "@/components/main/filter-publication-pemerintah-daerah";
export default async function FilterKl() {
return (
<>
<PublicationPemerintahDaerahLayout />
</>
);
}

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

@ -0,0 +1,9 @@
import Schedule from "@/components/landing-page/schedule";
export default async function FilterKl() {
return (
<>
<Schedule />
</>
);
}

21
components.json Normal file
View File

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@ -0,0 +1,41 @@
import React, { useRef, useState, useEffect } from "react";
interface AudioPlayerProps {
urlAudio: string;
fileName: string;
}
const AudioPlayer: React.FC<AudioPlayerProps> = ({ urlAudio, fileName }) => {
const audioRef = useRef<HTMLAudioElement>(null);
const [currentTime, setCurrentTime] = useState(0);
useEffect(() => {
const audio = audioRef.current;
if (!audio) return;
const updateTime = () => {
setCurrentTime(audio?.currentTime);
};
audio.addEventListener("timeupdate", updateTime);
return () => {
audio.removeEventListener("timeupdate", updateTime);
};
}, []);
return (
<div className="mt-2">
<h2 className="text-lg font-semibold">{fileName}</h2>
{/* <a href={urlAudio} target="_blank">{urlAudio}</a> */}
<audio ref={audioRef} src={urlAudio} controls className="mt-1 w-full" />
{/* <div className="mt-2 space-x-2">
<button onClick={playAudio}> Play</button>
<button onClick={pauseAudio}> Pause</button>
<button onClick={stopAudio}> Stop</button>
</div>
<div className="mt-1 text-sm text-gray-500">{formatTime(currentTime)}</div> */}
</div>
);
};
export default AudioPlayer;

View File

@ -0,0 +1,40 @@
"use client";
import React from "react";
import Image from "next/image";
import { AuthLayoutProps } from "@/types/auth";
import { cn } from "@/lib/utils";
import Link from "next/link";
export const AuthLayout: React.FC<AuthLayoutProps> = ({
children,
showSidebar = true,
className,
}) => {
return (
<div className="flex w-full items-center overflow-hidden min-h-dvh h-dvh basis-full">
<div className="overflow-y-auto flex flex-wrap w-full h-dvh">
{showSidebar && (
<div className="lg:block hidden flex-1 overflow-hidden text-[40px] leading-[48px] text-default-600 relative z-[1] bg-default-50">
<div className="max-w-[520px] pt-16 ps-20">
<Link href="/" className="mb-6 inline-block">
<img
src="/Group.png"
alt="Mikul News Logo"
className="max-w-2xl h-auto drop-shadow-lg"
/>
</Link>
</div>
</div>
)}
<div className={cn("flex-1 relative", className)}>
<div className="h-full flex flex-col dark:bg-default-100 bg-white">
<div className="max-w-[524px] md:px-[42px] md:py-[44px] p-7 mx-auto w-full text-2xl text-default-900 mb-3 h-full flex flex-col justify-center">
{children}
</div>
</div>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,130 @@
"use client";
import React from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@/components/ui/button";
import {
emailValidationSchema,
EmailValidationData,
EmailSetupFormProps,
} from "@/types/auth";
import { useEmailSetup } from "@/hooks/use-auth";
import { FormField } from "./form-field";
export const EmailSetupForm: React.FC<EmailSetupFormProps> = ({
loginCredentials,
onSuccess,
onError,
onBack,
className,
}) => {
const { setupEmail, loading } = useEmailSetup();
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
getValues,
} = useForm<EmailValidationData>({
resolver: zodResolver(emailValidationSchema),
mode: "onChange",
});
const onSubmit = async (data: EmailValidationData) => {
try {
if (!loginCredentials) {
onError?.("Login credentials not found. Please try logging in again.");
return;
}
const result = await setupEmail(loginCredentials, data);
switch (result) {
case "otp":
onSuccess?.();
break;
case "success":
onSuccess?.();
break;
default:
onError?.("Unexpected response from email setup");
}
} catch (error: any) {
onError?.(error.message || "Email setup failed");
}
};
return (
<div className={className}>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
{/* Header */}
<div className="text-left space-y-2">
<h1 className="font-semibold text-3xl text-left">
Anda perlu memasukkan email baru untuk bisa Login.
</h1>
<p className="text-default-500 text-base">
Please provide your old and new email addresses for verification.
</p>
</div>
{/* Old Email Field */}
<FormField
label="Email Lama"
name="oldEmail"
type="email"
placeholder="Enter your old email address"
error={errors.oldEmail?.message}
disabled={isSubmitting || loading}
required
inputProps={{
...register("oldEmail"),
}}
/>
{/* New Email Field */}
<FormField
label="Email Baru"
name="newEmail"
type="email"
placeholder="Enter your new email address"
error={errors.newEmail?.message}
disabled={isSubmitting || loading}
required
inputProps={{
...register("newEmail"),
}}
/>
{/* Action Buttons */}
<div className="flex gap-4 pt-4">
{onBack && (
<Button
type="button"
variant="outline"
onClick={onBack}
disabled={isSubmitting || loading}
className="flex-1"
>
Back
</Button>
)}
<Button
type="submit"
disabled={isSubmitting || loading}
className="flex-1 bg-red-500 hover:bg-red-600"
>
{isSubmitting || loading ? (
<div className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
Processing...
</div>
) : (
"Simpan"
)}
</Button>
</div>
</form>
</div>
);
};

View File

@ -0,0 +1,93 @@
"use client";
import React from "react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
import { Eye, EyeOff } from "lucide-react";
interface FormFieldProps {
label: string;
name: string;
type?: "text" | "email" | "password" | "tel" | "number";
placeholder?: string;
error?: string;
disabled?: boolean;
required?: boolean;
className?: string;
inputProps?: React.ComponentProps<typeof Input>;
showPasswordToggle?: boolean;
onPasswordToggle?: () => void;
showPassword?: boolean;
}
export const FormField: React.FC<FormFieldProps> = ({
label,
name,
type = "text",
placeholder,
error,
disabled = false,
required = false,
className,
inputProps,
showPasswordToggle = false,
onPasswordToggle,
showPassword = false,
}) => {
const inputType = showPasswordToggle && type === "password"
? (showPassword ? "text" : "password")
: type;
return (
<div className={cn("space-y-2", className)}>
<Label
htmlFor={name}
className="font-medium text-default-600"
>
{label}
{required && <span className="text-red-500 ml-1">*</span>}
</Label>
<div className="relative">
<Input
id={name}
name={name}
type={inputType}
placeholder={placeholder}
disabled={disabled}
className={cn(
"peer",
{
"border-destructive": error,
"pr-10": showPasswordToggle,
},
inputProps?.className
)}
aria-invalid={!!error}
aria-describedby={error ? `${name}-error` : undefined}
{...inputProps}
/>
{showPasswordToggle && (
<button
type="button"
onClick={onPasswordToggle}
className="absolute right-3 top-1/2 -translate-y-1/2 text-default-500 hover:text-default-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 rounded"
tabIndex={-1}
aria-label={showPassword ? "Hide password" : "Show password"}
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
)}
</div>
{error && (
<div
id={`${name}-error`}
className="text-destructive mt-2 text-sm"
role="alert"
>
{error}
</div>
)}
</div>
);
};

View File

@ -0,0 +1,217 @@
"use client";
import React, { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import {
Dialog,
DialogContent,
DialogFooter,
DialogTrigger,
} from "@/components/ui/dialog";
import { FormField } from "@/components/auth/form-field";
import { loginSchema, LoginFormData, LoginFormProps } from "@/types/auth";
import { useAuth } from "@/hooks/use-auth";
import { listRole } from "@/service/landing/landing";
import { Role } from "@/types/auth";
import Link from "next/link";
export const LoginForm: React.FC<LoginFormProps> = ({
onSuccess,
onError,
className,
}) => {
const { login } = useAuth();
const [showPassword, setShowPassword] = useState(false);
const [rememberMe, setRememberMe] = useState(true);
const [roles, setRoles] = useState<Role[]>([]);
const [selectedCategory, setSelectedCategory] = useState("5");
const [isDialogOpen, setIsDialogOpen] = useState(false);
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
getValues,
} = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
mode: "onChange",
});
// Load roles on component mount
React.useEffect(() => {
const loadRoles = async () => {
try {
const response = await listRole();
setRoles(response?.data?.data || []);
} catch (error) {
console.error("Failed to load roles:", error);
}
};
loadRoles();
}, []);
const handlePasswordToggle = () => {
setShowPassword(!showPassword);
};
const handleLogin = async (data: LoginFormData) => {
try {
await login(data);
onSuccess?.(data);
} catch (error: any) {
onError?.(error.message || "Login failed");
}
};
const onSubmit = async (data: LoginFormData) => {
try {
// Pass the form data to the parent component
// The auth page will handle email validation and flow transitions
onSuccess?.(data);
} catch (error: any) {
onError?.(error.message || "Login failed");
}
};
return (
<div className={className}>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
{/* Header */}
<div className="text-left space-y-2">
<div className="text-center mb-8">
<div className=" w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-8">
<img
src="/logo-netidhub.png"
alt="netidhub Logo"
className="max-w-[150px] h-auto drop-shadow-lg"
/>
</div>
<h2 className="text-lg font-bold text-gray-900 mb-2 mt-5">
MENYATUKAN INDONESIA
</h2>
</div>
{/* <div className="text-default-500 text-base">
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<span className="w-full lg:w-fit px-2 h-8 text-red-500 hover:cursor-pointer hover:underline">
Register
</span>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<div className="flex flex-col w-full gap-1">
<p className="text-lg font-semibold text-center">
Category Reg
</p>
<p className="text-base text-center">Select One</p>
</div>
<div className="space-y-2">
{roles.map((role) => (
<div key={role.id} className="flex items-center space-x-2">
<input
type="radio"
id={`category${role.id}`}
name="category"
value={role.id.toString()}
checked={selectedCategory === role.id.toString()}
onChange={(e) => setSelectedCategory(e.target.value)}
className="text-red-500 focus:ring-red-500"
/>
<Label htmlFor={`category${role.id}`} className="text-sm">
{role.name}
</Label>
</div>
))}
</div>
<div className="border-b-2 border-black"></div>
<DialogFooter>
<Link
href={`/auth/registration?category=${selectedCategory}`}
className="flex justify-center bg-red-500 px-4 py-2 rounded-md border border-black text-white hover:bg-red-600 transition-colors"
>
Next
</Link>
</DialogFooter>
</DialogContent>
</Dialog>
</div> */}
</div>
{/* Username Field */}
<FormField
label="Username"
name="username"
type="text"
placeholder="Enter your username"
error={errors.username?.message}
disabled={isSubmitting}
required
inputProps={{
...register("username"),
}}
/>
{/* Password Field */}
<FormField
label="Password"
name="password"
type="password"
placeholder="Enter your password"
error={errors.password?.message}
disabled={isSubmitting}
required
showPasswordToggle
showPassword={showPassword}
onPasswordToggle={handlePasswordToggle}
inputProps={{
...register("password"),
}}
/>
{/* Remember Me and Forgot Password */}
<div className="flex justify-between items-center">
<div className="flex gap-2 items-center">
<Checkbox
id="rememberMe"
checked={rememberMe}
onCheckedChange={(checked) => setRememberMe(checked as boolean)}
disabled={isSubmitting}
/>
<Label htmlFor="rememberMe" className="text-sm">
Remember Me
</Label>
</div>
<Link
href="/auth/forgot-password"
className="text-sm text-default-800 dark:text-default-400 leading-6 font-medium hover:underline"
>
Lupa kata sandi?
</Link>
</div>
{/* Submit Button */}
<Button
type="submit"
fullWidth
disabled={isSubmitting}
className="mt-6 bg-[#C6A455]"
color="primary"
>
{isSubmitting ? (
<div className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin " />
Processing...
</div>
) : (
"Selanjutnya"
)}
</Button>
</form>
</div>
);
};

View File

@ -0,0 +1,167 @@
"use client";
import React, { useState } from "react";
import { Button } from "@/components/ui/button";
import { OTPFormProps } from "@/types/auth";
import { useOTPVerification } from "@/hooks/use-auth";
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "../ui/input-otp";
export const OTPForm: React.FC<OTPFormProps> = ({
loginCredentials,
onSuccess,
onError,
onResend,
className,
}) => {
const { verifyOTP, loading } = useOTPVerification();
const [otpValue, setOtpValue] = useState("");
const handleTypeOTP = (event: React.KeyboardEvent<HTMLInputElement>) => {
const { key } = event;
const target = event.currentTarget;
if (key === "Enter") {
event.preventDefault();
const inputs = Array.from(target.form?.querySelectorAll("input") || []);
const currentIndex = inputs.indexOf(target);
const nextInput = inputs[currentIndex + 1] as HTMLElement | undefined;
if (nextInput) {
nextInput.focus();
}
}
};
const handleOTPChange = (value: string) => {
setOtpValue(value);
};
const handleSubmit = async () => {
if (otpValue.length !== 6) {
onError?.("Please enter a complete 6-digit OTP");
return;
}
if (!loginCredentials?.username) {
onError?.("Username not found. Please try logging in again.");
return;
}
try {
const isValid = await verifyOTP(loginCredentials.username, otpValue);
if (isValid) {
onSuccess?.();
} else {
onError?.("Invalid OTP code");
}
} catch (error: any) {
onError?.(error.message || "OTP verification failed");
}
};
const handleResend = () => {
onResend?.();
};
return (
<div className={className}>
<div className="space-y-6">
{/* Header */}
<div className="text-left space-y-2">
<h1 className="font-semibold text-3xl text-left">Please Enter OTP</h1>
<p className="text-default-500 text-base">
Enter the 6-digit code sent to your email address.
</p>
</div>
{/* OTP Input */}
<div className="flex justify-center">
<InputOTP
maxLength={6}
value={otpValue}
onChange={handleOTPChange}
disabled={loading}
className="gap-2"
>
<InputOTPGroup>
<InputOTPSlot
index={0}
onKeyDown={handleTypeOTP}
className="w-12 h-12 text-lg"
/>
<InputOTPSlot
index={1}
onKeyDown={handleTypeOTP}
className="w-12 h-12 text-lg"
/>
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot
index={2}
onKeyDown={handleTypeOTP}
className="w-12 h-12 text-lg"
/>
<InputOTPSlot
index={3}
onKeyDown={handleTypeOTP}
className="w-12 h-12 text-lg"
/>
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot
index={4}
onKeyDown={handleTypeOTP}
className="w-12 h-12 text-lg"
/>
<InputOTPSlot
index={5}
onKeyDown={handleTypeOTP}
className="w-12 h-12 text-lg"
/>
</InputOTPGroup>
</InputOTP>
</div>
{/* Resend OTP */}
<div className="text-center">
<button
type="button"
onClick={handleResend}
disabled={loading}
className="text-sm text-blue-600 hover:text-blue-800 underline disabled:opacity-50 disabled:cursor-not-allowed"
>
Didn't receive the code? Resend
</button>
</div>
{/* Submit Button */}
<Button
type="button"
fullWidth
onClick={handleSubmit}
disabled={otpValue.length !== 6 || loading}
className="bg-[#C6A455] hover:bg-black"
color="primary"
>
{loading ? (
<div className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
Verifying...
</div>
) : (
"Sign In"
)}
</Button>
</div>
</div>
);
};

Binary file not shown.

View File

@ -0,0 +1,41 @@
// components/custom-editor.js
import React from "react";
import { CKEditor } from "@ckeditor/ckeditor5-react";
import Editor from "@/vendor/ckeditor5/build/ckeditor";
function CustomEditor(props) {
return (
<CKEditor
editor={Editor}
data={props.initialData}
onChange={(event, editor) => {
const data = editor.getData();
console.log({ event, editor, data });
props.onChange(data);
}}
config={{
toolbar: [
"heading",
"fontsize",
"bold",
"italic",
"link",
"numberedList",
"bulletedList",
"undo",
"redo",
"alignment",
"outdent",
"indent",
"blockQuote",
"insertTable",
"codeBlock",
"sourceEditing",
],
}}
/>
);
}
export default CustomEditor;

View File

@ -0,0 +1,164 @@
"use client";
import React, { useState } from 'react';
// Import the optimized editor (choose one based on your migration)
// import OptimizedEditor from './optimized-editor'; // TinyMCE
// import OptimizedCKEditor from './optimized-ckeditor'; // CKEditor5 Classic
// import MinimalEditor from './minimal-editor'; // React Quill
interface EditorExampleProps {
editorType?: 'tinymce' | 'ckeditor' | 'quill';
}
const EditorExample: React.FC<EditorExampleProps> = ({
editorType = 'tinymce'
}) => {
const [content, setContent] = useState('<p>Hello, this is the editor content!</p>');
const [savedContent, setSavedContent] = useState('');
const handleContentChange = (newContent: string) => {
setContent(newContent);
};
const handleSave = () => {
setSavedContent(content);
console.log('Content saved:', content);
};
const handleReset = () => {
setContent('<p>Content has been reset!</p>');
};
return (
<div className="p-6 max-w-4xl mx-auto">
<div className="mb-6">
<h2 className="text-2xl font-bold mb-4">Rich Text Editor Example</h2>
<p className="text-gray-600 mb-4">
This is an optimized editor with {editorType} - much smaller bundle size and better performance!
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Editor Panel */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">Editor</h3>
<div className="flex gap-2">
<button
onClick={handleSave}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Save
</button>
<button
onClick={handleReset}
className="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
>
Reset
</button>
</div>
</div>
<div className="border border-gray-200 rounded-lg">
{/* Choose your editor based on migration */}
{editorType === 'tinymce' && (
<div className="p-4">
<p className="text-gray-500 text-sm mb-2">
TinyMCE Editor (200KB bundle)
</p>
{/* <OptimizedEditor
initialData={content}
onChange={handleContentChange}
height={400}
placeholder="Start typing your content..."
/> */}
<div className="h-96 bg-gray-50 border border-gray-200 rounded flex items-center justify-center">
<p className="text-gray-500">TinyMCE Editor Component</p>
</div>
</div>
)}
{editorType === 'ckeditor' && (
<div className="p-4">
<p className="text-gray-500 text-sm mb-2">
CKEditor5 Classic (800KB bundle)
</p>
{/* <OptimizedCKEditor
initialData={content}
onChange={handleContentChange}
height={400}
placeholder="Start typing your content..."
/> */}
<div className="h-96 bg-gray-50 border border-gray-200 rounded flex items-center justify-center">
<p className="text-gray-500">CKEditor5 Classic Component</p>
</div>
</div>
)}
{editorType === 'quill' && (
<div className="p-4">
<p className="text-gray-500 text-sm mb-2">
React Quill (100KB bundle)
</p>
{/* <MinimalEditor
initialData={content}
onChange={handleContentChange}
height={400}
placeholder="Start typing your content..."
/> */}
<div className="h-96 bg-gray-50 border border-gray-200 rounded flex items-center justify-center">
<p className="text-gray-500">React Quill Component</p>
</div>
</div>
)}
</div>
</div>
{/* Preview Panel */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">Preview</h3>
<div className="border border-gray-200 rounded-lg p-4">
<h4 className="font-medium mb-2">Current Content:</h4>
<div
className="prose max-w-none"
dangerouslySetInnerHTML={{ __html: content }}
/>
</div>
{savedContent && (
<div className="border border-gray-200 rounded-lg p-4">
<h4 className="font-medium mb-2">Saved Content:</h4>
<div
className="prose max-w-none"
dangerouslySetInnerHTML={{ __html: savedContent }}
/>
</div>
)}
<div className="border border-gray-200 rounded-lg p-4">
<h4 className="font-medium mb-2">Raw HTML:</h4>
<pre className="text-xs bg-gray-100 p-2 rounded overflow-auto max-h-32">
{content}
</pre>
</div>
</div>
</div>
{/* Performance Info */}
<div className="mt-8 p-4 bg-blue-50 rounded-lg">
<h4 className="font-medium text-blue-900 mb-2">Performance Benefits:</h4>
<ul className="text-sm text-blue-800 space-y-1">
<li> 90% smaller bundle size compared to custom CKEditor5</li>
<li> Faster initial load time</li>
<li> Better mobile performance</li>
<li> Reduced memory usage</li>
<li> Improved Lighthouse score</li>
</ul>
</div>
</div>
);
};
export default EditorExample;

View File

@ -0,0 +1,176 @@
"use client";
import React, { useState } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card } from '@/components/ui/card';
import CustomEditor from './custom-editor';
import FormEditor from './form-editor';
export default function EditorTest() {
const [testData, setTestData] = useState('Initial test content');
const [editorType, setEditorType] = useState('custom');
const { control, setValue, watch, handleSubmit } = useForm({
defaultValues: {
title: 'Test Title',
description: testData,
creatorName: 'Test Creator'
}
});
const watchedValues = watch();
const handleSetValue = () => {
const newContent = `<p>Updated content at ${new Date().toLocaleTimeString()}</p><p>This content was set via setValue</p>`;
setValue('description', newContent);
setTestData(newContent);
};
const handleSetEmpty = () => {
setValue('description', '');
setTestData('');
};
const handleSetHTML = () => {
const htmlContent = `
<h2>HTML Content Test</h2>
<p>This is a <strong>bold</strong> paragraph with <em>italic</em> text.</p>
<ul>
<li>List item 1</li>
<li>List item 2</li>
<li>List item 3</li>
</ul>
<p>Updated at: ${new Date().toLocaleTimeString()}</p>
`;
setValue('description', htmlContent);
setTestData(htmlContent);
};
const onSubmit = (data: any) => {
console.log('Form submitted:', data);
alert('Form submitted! Check console for data.');
};
return (
<div className="p-6 max-w-4xl mx-auto space-y-6">
<h1 className="text-2xl font-bold">Editor Test Component</h1>
<Card className="p-4">
<div className="space-y-4">
<div>
<Label>Editor Type:</Label>
<div className="flex gap-2 mt-2">
<Button
variant={editorType === 'custom' ? 'default' : 'outline'}
onClick={() => setEditorType('custom')}
>
CustomEditor
</Button>
<Button
variant={editorType === 'form' ? 'default' : 'outline'}
onClick={() => setEditorType('form')}
>
FormEditor
</Button>
</div>
</div>
<div className="flex gap-2 flex-wrap">
<Button onClick={handleSetValue} variant="outline">
Set Value (Current Time)
</Button>
<Button onClick={handleSetEmpty} variant="outline">
Set Empty
</Button>
<Button onClick={handleSetHTML} variant="outline">
Set HTML Content
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label>Current Test Data:</Label>
<div className="mt-2 p-2 bg-gray-100 rounded text-sm">
{testData || '(empty)'}
</div>
</div>
<div>
<Label>Watched Form Values:</Label>
<div className="mt-2 p-2 bg-gray-100 rounded text-sm">
<pre>{JSON.stringify(watchedValues, null, 2)}</pre>
</div>
</div>
</div>
</div>
</Card>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
<Card className="p-4">
<div className="space-y-4">
<div>
<Label>Title:</Label>
<Controller
control={control}
name="title"
render={({ field }) => (
<Input {...field} className="mt-1" />
)}
/>
</div>
<div>
<Label>Description (Editor):</Label>
<Controller
control={control}
name="description"
render={({ field }) => (
editorType === 'custom' ? (
<CustomEditor
onChange={field.onChange}
initialData={field.value}
/>
) : (
<FormEditor
onChange={field.onChange}
initialData={field.value}
/>
)
)}
/>
</div>
<div>
<Label>Creator Name:</Label>
<Controller
control={control}
name="creatorName"
render={({ field }) => (
<Input {...field} className="mt-1" />
)}
/>
</div>
<Button type="submit" className="w-full">
Submit Form
</Button>
</div>
</Card>
</form>
<Card className="p-4">
<h3 className="font-semibold mb-2">Instructions:</h3>
<ul className="list-disc list-inside space-y-1 text-sm">
<li>Switch between CustomEditor and FormEditor to test both</li>
<li>Click "Set Value" to test setValue functionality</li>
<li>Click "Set Empty" to test empty content handling</li>
<li>Click "Set HTML Content" to test rich HTML content</li>
<li>Type in the editor to test onChange functionality</li>
<li>Submit the form to see all data</li>
</ul>
</Card>
</div>
);
}

Binary file not shown.

View File

@ -0,0 +1,102 @@
import React, { useRef, useEffect, useState, useCallback } from "react";
import { Editor } from "@tinymce/tinymce-react";
function FormEditor({ onChange, initialData }) {
const editorRef = useRef(null);
const [isEditorReady, setIsEditorReady] = useState(false);
const [editorContent, setEditorContent] = useState(initialData || "");
// Handle editor initialization
const handleInit = useCallback((evt, editor) => {
editorRef.current = editor;
setIsEditorReady(true);
// Set initial content when editor is ready
if (editorContent) {
editor.setContent(editorContent);
}
// Handle content changes
editor.on('change', () => {
const content = editor.getContent();
setEditorContent(content);
if (onChange) {
onChange(content);
}
});
}, [editorContent, onChange]);
// Watch for initialData changes (from setValue)
useEffect(() => {
if (initialData !== editorContent) {
setEditorContent(initialData || "");
// Update editor content if ready
if (editorRef.current && isEditorReady) {
editorRef.current.setContent(initialData || "");
}
}
}, [initialData, editorContent, isEditorReady]);
// Handle initial data when editor becomes ready
useEffect(() => {
if (isEditorReady && editorContent && editorRef.current) {
editorRef.current.setContent(editorContent);
}
}, [isEditorReady, editorContent]);
return (
<Editor
onInit={handleInit}
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
init={{
height: 400,
menubar: false,
plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
],
toolbar: 'undo redo | blocks | ' +
'bold italic forecolor | alignleft aligncenter ' +
'alignright alignjustify | bullist numlist outdent indent | ' +
'removeformat | table | code | help',
content_style: `
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
line-height: 1.6;
color: #333;
}
.mce-content-body {
padding: 16px;
min-height: 368px;
}
`,
placeholder: 'Start typing...',
branding: false,
elementpath: false,
resize: false,
statusbar: false,
auto_focus: false,
forced_root_block: 'p',
entity_encoding: 'raw',
verify_html: false,
cleanup: false,
cleanup_on_startup: false,
auto_resize: false,
paste_as_text: false,
paste_enable_default_filters: true,
paste_word_valid_elements: 'b,strong,i,em,h1,h2,h3,h4,h5,h6',
paste_retain_style_properties: 'color background-color font-size font-weight',
mobile: {
theme: 'silver',
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
toolbar: 'bold italic | bullist numlist | link image'
}
}}
/>
);
}
export default FormEditor;

View File

@ -0,0 +1,81 @@
// components/minimal-editor.js
import React, { useRef } from "react";
import { Editor } from "@tinymce/tinymce-react";
function MinimalEditor(props) {
const editorRef = useRef(null);
const handleInit = (evt, editor) => {
editorRef.current = editor;
// Set initial content if provided
if (props.initialData) {
editor.setContent(props.initialData);
}
// Simple onChange handler - no debouncing, no complex logic
editor.on('change', () => {
if (props.onChange) {
props.onChange(editor.getContent());
}
});
};
return (
<Editor
onInit={handleInit}
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
init={{
height: 400,
menubar: false,
plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
],
toolbar: 'undo redo | blocks | ' +
'bold italic forecolor | alignleft aligncenter ' +
'alignright alignjustify | bullist numlist outdent indent | ' +
'removeformat | table | code | help',
content_style: `
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
line-height: 1.6;
color: #333;
}
.mce-content-body {
padding: 16px;
min-height: 368px;
}
`,
placeholder: 'Start typing...',
branding: false,
elementpath: false,
resize: false,
statusbar: false,
// Minimal settings to prevent cursor jumping
auto_focus: false,
forced_root_block: 'p',
entity_encoding: 'raw',
// Disable problematic features
verify_html: false,
cleanup: false,
cleanup_on_startup: false,
auto_resize: false,
// Basic content handling
paste_as_text: false,
paste_enable_default_filters: true,
// Mobile support
mobile: {
theme: 'silver',
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
toolbar: 'bold italic | bullist numlist | link image'
}
}}
/>
);
}
export default MinimalEditor;

View File

@ -0,0 +1,89 @@
"use client";
import React, { useEffect, useRef } from 'react';
import { Editor } from '@tinymce/tinymce-react';
interface OptimizedEditorProps {
initialData?: string;
onChange?: (data: string) => void;
height?: number;
placeholder?: string;
disabled?: boolean;
readOnly?: boolean;
}
const OptimizedEditor: React.FC<OptimizedEditorProps> = ({
initialData = '',
onChange,
height = 400,
placeholder = 'Start typing...',
disabled = false,
readOnly = false,
}) => {
const editorRef = useRef<any>(null);
const handleEditorChange = (content: string) => {
if (onChange) {
onChange(content);
}
};
const handleInit = (evt: any, editor: any) => {
editorRef.current = editor;
};
return (
<Editor
onInit={handleInit}
initialValue={initialData}
onEditorChange={handleEditorChange}
disabled={disabled}
init={{
height,
menubar: false,
plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
],
toolbar: 'undo redo | blocks | ' +
'bold italic forecolor | alignleft aligncenter ' +
'alignright alignjustify | bullist numlist outdent indent | ' +
'removeformat | table | code | help',
content_style: `
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
line-height: 1.6;
color: #333;
}
.mce-content-body {
padding: 16px;
min-height: ${height - 32}px;
}
`,
placeholder,
readonly: readOnly,
branding: false,
elementpath: false,
resize: false,
statusbar: false,
// Performance optimizations
cache_suffix: '?v=1.0',
browser_spellcheck: false,
gecko_spellcheck: false,
// Auto-save feature
auto_save: true,
auto_save_interval: '30s',
// Better mobile support
mobile: {
theme: 'silver',
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
toolbar: 'bold italic | bullist numlist | link image'
}
}}
/>
);
};
export default OptimizedEditor;

View File

@ -0,0 +1,136 @@
// components/readonly-editor.js
import React, { useRef, useEffect } from "react";
import { Editor } from "@tinymce/tinymce-react";
function ReadOnlyEditor(props) {
const editorRef = useRef(null);
const handleInit = (evt, editor) => {
editorRef.current = editor;
// Set initial content if provided
if (props.initialData) {
editor.setContent(props.initialData);
}
// Disable all editing capabilities
editor.on('keydown keyup keypress input', (e) => {
e.preventDefault();
e.stopPropagation();
return false;
});
editor.on('paste', (e) => {
e.preventDefault();
e.stopPropagation();
return false;
});
editor.on('drop', (e) => {
e.preventDefault();
e.stopPropagation();
return false;
});
// Disable mouse events that might allow editing
editor.on('mousedown mousemove mouseup click dblclick', (e) => {
if (e.target.closest('.mce-content-body')) {
e.preventDefault();
e.stopPropagation();
return false;
}
});
};
// Update content when props change
useEffect(() => {
if (editorRef.current && props.initialData) {
editorRef.current.setContent(props.initialData);
}
}, [props.initialData]);
return (
<Editor
onInit={handleInit}
initialValue={props.initialData || ''}
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
init={{
height: props.height || 400,
menubar: false,
toolbar: false, // No toolbar for read-only mode
plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap',
'anchor', 'searchreplace', 'visualblocks', 'code',
'insertdatetime', 'media', 'table'
],
content_style: `
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
line-height: 1.6;
color: #333;
pointer-events: none;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.mce-content-body {
padding: 16px;
min-height: ${(props.height || 400) - 32}px;
pointer-events: none;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.mce-content-body * {
pointer-events: none;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
`,
readonly: true,
branding: false,
elementpath: false,
resize: false,
statusbar: false,
// Minimal settings to prevent cursor jumping
auto_focus: false,
forced_root_block: 'p',
entity_encoding: 'raw',
// Disable problematic features
verify_html: false,
cleanup: false,
cleanup_on_startup: false,
auto_resize: false,
// Performance optimizations for read-only
cache_suffix: '?v=1.0',
browser_spellcheck: false,
gecko_spellcheck: false,
// Disable editing features
paste_as_text: true,
paste_enable_default_filters: false,
paste_word_valid_elements: false,
paste_retain_style_properties: false,
// Additional read-only settings
contextmenu: false,
selection: false,
// Disable all editing
object_resizing: false,
element_format: 'html',
// Mobile support
mobile: {
theme: 'silver',
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
toolbar: false
}
}}
/>
);
}
export default ReadOnlyEditor;

View File

@ -0,0 +1,95 @@
// components/simple-editor.js
import React, { useRef, useState, useCallback } from "react";
import { Editor } from "@tinymce/tinymce-react";
function SimpleEditor(props) {
const editorRef = useRef(null);
const [editorInstance, setEditorInstance] = useState(null);
const handleInit = useCallback((evt, editor) => {
editorRef.current = editor;
setEditorInstance(editor);
// Set initial content
if (props.initialData) {
editor.setContent(props.initialData);
}
// Disable automatic content updates
editor.settings.auto_focus = false;
editor.settings.forced_root_block = 'p';
// Store the onChange callback
editor.onChangeCallback = props.onChange;
// Handle content changes without triggering re-renders
editor.on('change keyup input', (e) => {
if (editor.onChangeCallback) {
const content = editor.getContent();
editor.onChangeCallback(content);
}
});
}, [props.initialData, props.onChange]);
return (
<Editor
onInit={handleInit}
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
init={{
height: 400,
menubar: false,
plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
],
toolbar: 'undo redo | blocks | ' +
'bold italic forecolor | alignleft aligncenter ' +
'alignright alignjustify | bullist numlist outdent indent | ' +
'removeformat | table | code | help',
content_style: `
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
line-height: 1.6;
color: #333;
}
.mce-content-body {
padding: 16px;
min-height: 368px;
}
`,
placeholder: 'Start typing...',
branding: false,
elementpath: false,
resize: false,
statusbar: false,
// Critical settings to prevent cursor jumping
auto_focus: false,
forced_root_block: 'p',
entity_encoding: 'raw',
keep_styles: true,
// Disable problematic features
verify_html: false,
cleanup: false,
cleanup_on_startup: false,
auto_resize: false,
// Better content handling
paste_as_text: false,
paste_enable_default_filters: true,
paste_word_valid_elements: 'b,strong,i,em,h1,h2,h3,h4,h5,h6',
paste_retain_style_properties: 'color background-color font-size font-weight',
// Mobile support
mobile: {
theme: 'silver',
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
toolbar: 'bold italic | bullist numlist | link image'
}
}}
/>
);
}
export default SimpleEditor;

View File

@ -0,0 +1,109 @@
// components/simple-readonly-editor.js
import React, { useRef } from "react";
import { Editor } from "@tinymce/tinymce-react";
function SimpleReadOnlyEditor(props) {
const editorRef = useRef(null);
const handleInit = (evt, editor) => {
editorRef.current = editor;
// Disable all editing capabilities
editor.on('keydown keyup keypress input', (e) => {
e.preventDefault();
e.stopPropagation();
return false;
});
editor.on('paste', (e) => {
e.preventDefault();
e.stopPropagation();
return false;
});
editor.on('drop', (e) => {
e.preventDefault();
e.stopPropagation();
return false;
});
// Disable mouse events that might allow editing
editor.on('mousedown mousemove mouseup click dblclick', (e) => {
if (e.target.closest('.mce-content-body')) {
e.preventDefault();
e.stopPropagation();
return false;
}
});
};
return (
<Editor
onInit={handleInit}
initialValue={props.initialData || ''}
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
init={{
height: props.height || 400,
menubar: false,
toolbar: false,
plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap',
'anchor', 'searchreplace', 'visualblocks', 'code',
'insertdatetime', 'media', 'table'
],
content_style: `
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
line-height: 1.6;
color: #333;
pointer-events: none;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.mce-content-body {
padding: 16px;
min-height: ${(props.height || 400) - 32}px;
pointer-events: none;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.mce-content-body * {
pointer-events: none;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
`,
readonly: true,
branding: false,
elementpath: false,
resize: false,
statusbar: false,
auto_focus: false,
forced_root_block: 'p',
entity_encoding: 'raw',
verify_html: false,
cleanup: false,
cleanup_on_startup: false,
auto_resize: false,
browser_spellcheck: false,
gecko_spellcheck: false,
paste_as_text: true,
paste_enable_default_filters: false,
contextmenu: false,
selection: false,
object_resizing: false,
element_format: 'html'
}}
/>
);
}
export default SimpleReadOnlyEditor;

View File

@ -0,0 +1,93 @@
import React, { useRef, useEffect } from "react";
import { Editor } from "@tinymce/tinymce-react";
function StableEditor(props) {
const editorRef = useRef(null);
const onChangeRef = useRef(props.onChange);
// Update onChange ref when props change
useEffect(() => {
onChangeRef.current = props.onChange;
}, [props.onChange]);
const handleInit = (evt, editor) => {
editorRef.current = editor;
// Set initial content if provided
if (props.initialData) {
editor.setContent(props.initialData);
}
// Use a simple change handler that doesn't trigger re-renders
editor.on('change', () => {
if (onChangeRef.current) {
onChangeRef.current(editor.getContent());
}
});
};
return (
<Editor
onInit={handleInit}
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
init={{
height: 400,
menubar: false,
plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
],
toolbar: 'undo redo | blocks | ' +
'bold italic forecolor | alignleft aligncenter ' +
'alignright alignjustify | bullist numlist outdent indent | ' +
'removeformat | table | code | help',
content_style: `
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
line-height: 1.6;
color: #333;
}
.mce-content-body {
padding: 16px;
min-height: 368px;
}
`,
placeholder: 'Start typing...',
branding: false,
elementpath: false,
resize: false,
statusbar: false,
// Critical settings for stability
auto_focus: false,
forced_root_block: 'p',
entity_encoding: 'raw',
keep_styles: true,
// Disable all problematic features
verify_html: false,
cleanup: false,
cleanup_on_startup: false,
auto_resize: false,
// Content handling
paste_as_text: false,
paste_enable_default_filters: true,
paste_word_valid_elements: 'b,strong,i,em,h1,h2,h3,h4,h5,h6',
paste_retain_style_properties: 'color background-color font-size font-weight',
// Prevent automatic updates
element_format: 'html',
valid_children: '+body[style]',
extended_valid_elements: 'span[*]',
custom_elements: '~span',
// Mobile support
mobile: {
theme: 'silver',
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
toolbar: 'bold italic | bullist numlist | link image'
}
}}
/>
);
}
export default StableEditor;

View File

@ -0,0 +1,93 @@
import React, { useRef, useEffect } from "react";
import { Editor } from "@tinymce/tinymce-react";
function StaticEditor(props) {
const editorRef = useRef(null);
const onChangeRef = useRef(props.onChange);
// Update onChange ref when props change
useEffect(() => {
onChangeRef.current = props.onChange;
}, [props.onChange]);
const handleInit = (evt, editor) => {
editorRef.current = editor;
// Set initial content if provided
if (props.initialData) {
editor.setContent(props.initialData);
}
// Use a simple change handler that doesn't trigger re-renders
editor.on('change', () => {
if (onChangeRef.current) {
onChangeRef.current(editor.getContent());
}
});
};
return (
<Editor
onInit={handleInit}
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
init={{
height: 400,
menubar: false,
plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
],
toolbar: 'undo redo | blocks | ' +
'bold italic forecolor | alignleft aligncenter ' +
'alignright alignjustify | bullist numlist outdent indent | ' +
'removeformat | table | code | help',
content_style: `
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
line-height: 1.6;
color: #333;
}
.mce-content-body {
padding: 16px;
min-height: 368px;
}
`,
placeholder: 'Start typing...',
branding: false,
elementpath: false,
resize: false,
statusbar: false,
// Critical settings to prevent cursor jumping
auto_focus: false,
forced_root_block: 'p',
entity_encoding: 'raw',
keep_styles: true,
// Disable all problematic features
verify_html: false,
cleanup: false,
cleanup_on_startup: false,
auto_resize: false,
// Content handling
paste_as_text: false,
paste_enable_default_filters: true,
paste_word_valid_elements: 'b,strong,i,em,h1,h2,h3,h4,h5,h6',
paste_retain_style_properties: 'color background-color font-size font-weight',
// Prevent automatic updates
element_format: 'html',
valid_children: '+body[style]',
extended_valid_elements: 'span[*]',
custom_elements: '~span',
// Mobile support
mobile: {
theme: 'silver',
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
toolbar: 'bold italic | bullist numlist | link image'
}
}}
/>
);
}
export default StaticEditor;

View File

@ -0,0 +1,113 @@
// components/strict-readonly-editor.js
import React, { useRef } from "react";
import { Editor } from "@tinymce/tinymce-react";
function StrictReadOnlyEditor(props) {
const editorRef = useRef(null);
const handleInit = (evt, editor) => {
editorRef.current = editor;
// Disable all possible editing events
const disableEvents = ['keydown', 'keyup', 'keypress', 'input', 'paste', 'drop', 'cut', 'copy'];
disableEvents.forEach(eventType => {
editor.on(eventType, (e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
return false;
});
});
// Disable mouse events that might allow editing
editor.on('mousedown mousemove mouseup click dblclick', (e) => {
if (e.target.closest('.mce-content-body')) {
e.preventDefault();
e.stopPropagation();
return false;
}
});
// Disable focus events
editor.on('focus blur', (e) => {
e.preventDefault();
e.stopPropagation();
return false;
});
};
return (
<Editor
onInit={handleInit}
initialValue={props.initialData || ''}
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
init={{
height: props.height || 400,
menubar: false,
toolbar: false,
plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap',
'anchor', 'searchreplace', 'visualblocks', 'code',
'insertdatetime', 'media', 'table'
],
content_style: `
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
line-height: 1.6;
color: #333;
pointer-events: none !important;
user-select: none !important;
-webkit-user-select: none !important;
-moz-user-select: none !important;
-ms-user-select: none !important;
}
.mce-content-body {
padding: 16px;
min-height: ${(props.height || 400) - 32}px;
pointer-events: none !important;
user-select: none !important;
-webkit-user-select: none !important;
-moz-user-select: none !important;
-ms-user-select: none !important;
}
.mce-content-body * {
pointer-events: none !important;
user-select: none !important;
-webkit-user-select: none !important;
-moz-user-select: none !important;
-ms-user-select: none !important;
}
`,
readonly: true,
branding: false,
elementpath: false,
resize: false,
statusbar: false,
auto_focus: false,
forced_root_block: 'p',
entity_encoding: 'raw',
verify_html: false,
cleanup: false,
cleanup_on_startup: false,
auto_resize: false,
browser_spellcheck: false,
gecko_spellcheck: false,
paste_as_text: true,
paste_enable_default_filters: false,
contextmenu: false,
selection: false,
object_resizing: false,
element_format: 'html',
// Additional strict settings
valid_children: false,
extended_valid_elements: false,
custom_elements: false
}}
/>
);
}
export default StrictReadOnlyEditor;

View File

@ -0,0 +1,264 @@
"use client";
import React, { useRef, useState, useEffect } from 'react';
import { Editor } from '@tinymce/tinymce-react';
interface TinyMCEEditorProps {
initialData?: string;
onChange?: (data: string) => void;
onReady?: (editor: any) => void;
height?: number;
placeholder?: string;
disabled?: boolean;
readOnly?: boolean;
features?: 'basic' | 'standard' | 'full';
toolbar?: string;
language?: string;
uploadUrl?: string;
uploadHeaders?: Record<string, string>;
className?: string;
autoSave?: boolean;
autoSaveInterval?: number;
}
const TinyMCEEditor: React.FC<TinyMCEEditorProps> = ({
initialData = '',
onChange,
onReady,
height = 400,
placeholder = 'Start typing...',
disabled = false,
readOnly = false,
features = 'standard',
toolbar,
language = 'en',
uploadUrl,
uploadHeaders,
className = '',
autoSave = true,
autoSaveInterval = 30000
}) => {
const editorRef = useRef<any>(null);
const [isEditorLoaded, setIsEditorLoaded] = useState(false);
const [lastSaved, setLastSaved] = useState<Date | null>(null);
const [wordCount, setWordCount] = useState(0);
// Feature-based configurations
const getFeatureConfig = (featureLevel: string) => {
const configs = {
basic: {
plugins: ['lists', 'link', 'autolink', 'wordcount'],
toolbar: 'bold italic | bullist numlist | link',
menubar: false
},
standard: {
plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
'insertdatetime', 'media', 'table', 'help', 'wordcount'
],
toolbar: 'undo redo | blocks | ' +
'bold italic forecolor | alignleft aligncenter ' +
'alignright alignjustify | bullist numlist outdent indent | ' +
'removeformat | table | code | help',
menubar: false
},
full: {
plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
'insertdatetime', 'media', 'table', 'help', 'wordcount', 'emoticons',
'paste', 'textcolor', 'colorpicker', 'hr', 'pagebreak', 'nonbreaking',
'toc', 'imagetools', 'textpattern', 'codesample'
],
toolbar: 'undo redo | formatselect | bold italic backcolor | ' +
'alignleft aligncenter alignright alignjustify | ' +
'bullist numlist outdent indent | removeformat | help',
menubar: 'file edit view insert format tools table help'
}
};
return configs[featureLevel as keyof typeof configs] || configs.standard;
};
const handleEditorChange = (content: string) => {
if (onChange) {
onChange(content);
}
};
const handleEditorInit = (evt: any, editor: any) => {
editorRef.current = editor;
setIsEditorLoaded(true);
if (onReady) {
onReady(editor);
}
// Set up word count tracking
editor.on('keyup', () => {
const count = editor.plugins.wordcount.body.getCharacterCount();
setWordCount(count);
});
// Set up auto-save
if (autoSave && !readOnly) {
setInterval(() => {
const content = editor.getContent();
localStorage.setItem('tinymce-autosave', content);
setLastSaved(new Date());
}, autoSaveInterval);
}
// Fix cursor jumping issues
editor.on('keyup', (e: any) => {
// Prevent cursor jumping on content changes
e.stopPropagation();
});
editor.on('input', (e: any) => {
// Prevent unnecessary re-renders
e.stopPropagation();
});
// Handle paste events properly
editor.on('paste', (e: any) => {
// Allow default paste behavior
return true;
});
};
const handleImageUpload = (blobInfo: any, progress: any) => {
return new Promise((resolve, reject) => {
if (!uploadUrl) {
reject('No upload URL configured');
return;
}
const formData = new FormData();
formData.append('file', blobInfo.blob(), blobInfo.filename());
fetch(uploadUrl, {
method: 'POST',
headers: uploadHeaders || {},
body: formData
})
.then(response => response.json())
.then(result => {
resolve(result.url);
})
.catch(error => {
reject(error);
});
});
};
const featureConfig = getFeatureConfig(features);
const editorConfig = {
height,
language,
placeholder,
readonly: readOnly,
disabled,
branding: false,
elementpath: false,
resize: false,
statusbar: !readOnly,
// Performance optimizations
cache_suffix: '?v=1.0',
browser_spellcheck: false,
gecko_spellcheck: false,
// Content styling
content_style: `
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
line-height: 1.6;
color: #333;
margin: 0;
padding: 16px;
}
.mce-content-body {
min-height: ${height - 32}px;
}
.mce-content-body:focus {
outline: none;
}
`,
// Image upload configuration
images_upload_handler: uploadUrl ? handleImageUpload : undefined,
automatic_uploads: !!uploadUrl,
file_picker_types: 'image',
// Better mobile support
mobile: {
theme: 'silver',
plugins: ['lists', 'autolink', 'link', 'image', 'table'],
toolbar: 'bold italic | bullist numlist | link image'
},
// Paste configuration
paste_as_text: false,
paste_enable_default_filters: true,
paste_word_valid_elements: 'b,strong,i,em,h1,h2,h3,h4,h5,h6',
paste_retain_style_properties: 'color background-color font-size font-weight',
// Table configuration
table_default_styles: {
width: '100%'
},
table_default_attributes: {
border: '1'
},
// Code configuration
codesample_languages: [
{ text: 'HTML/XML', value: 'markup' },
{ text: 'JavaScript', value: 'javascript' },
{ text: 'CSS', value: 'css' },
{ text: 'PHP', value: 'php' },
{ text: 'Python', value: 'python' },
{ text: 'Java', value: 'java' },
{ text: 'C', value: 'c' },
{ text: 'C++', value: 'cpp' }
],
// ...feature config
...featureConfig,
// Custom toolbar if provided
...(toolbar && { toolbar })
};
return (
<div className={`tinymce-editor-container ${className}`}>
<Editor
onInit={handleEditorInit}
initialValue={initialData}
onEditorChange={handleEditorChange}
disabled={disabled}
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
init={editorConfig}
/>
{/* Status bar */}
{isEditorLoaded && (
<div className="text-xs text-gray-500 mt-2 flex items-center justify-between">
<div className="flex items-center space-x-4">
<span>
{autoSave && !readOnly ? 'Auto-save enabled' : 'Read-only mode'}
</span>
{lastSaved && autoSave && !readOnly && (
<span> Last saved: {lastSaved.toLocaleTimeString()}</span>
)}
<span> {wordCount} characters</span>
</div>
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
{features} mode
</span>
</div>
)}
{/* Performance indicator */}
<div className="text-xs text-gray-400 mt-1">
Bundle size: {features === 'basic' ? '~150KB' : features === 'standard' ? '~200KB' : '~300KB'}
</div>
</div>
);
};
export default TinyMCEEditor;

View File

@ -0,0 +1,19 @@
import React from "react";
import { CKEditor } from "@ckeditor/ckeditor5-react";
import Editor from "@/vendor/ckeditor5/build/ckeditor";
function ViewEditor(props) {
return (
<CKEditor
editor={Editor}
data={props.initialData}
disabled={true}
config={{
// toolbar: [],
isReadOnly: true,
}}
/>
);
}
export default ViewEditor;

View File

@ -0,0 +1,907 @@
"use client";
import React, { ChangeEvent, useEffect, useRef, useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Card } from "@/components/ui/card";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { useParams, useRouter } from "next/navigation";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { register } from "module";
import { Switch } from "@/components/ui/switch";
import Cookies from "js-cookie";
import {
createMedia,
getTagsBySubCategoryId,
listEnableCategory,
rejectFiles,
submitApproval,
} from "@/service/content/content";
import {
detailMedia,
getDataApprovalByMediaUpload,
} from "@/service/curated-content/curated-content";
import { Badge } from "@/components/ui/badge";
import { MailIcon } from "lucide-react";
import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css";
import "swiper/css/free-mode";
import "swiper/css/navigation";
import "swiper/css/pagination";
import "swiper/css/thumbs";
import "swiper/css";
import "swiper/css/navigation";
import { FreeMode, Navigation, Pagination, Thumbs } from "swiper/modules";
import {
DialogHeader,
DialogFooter,
Dialog,
DialogContent,
DialogTitle,
} from "@/components/ui/dialog";
import { Textarea } from "@/components/ui/textarea";
import { loading } from "@/config/swal";
import { getCookiesDecrypt } from "@/lib/utils";
import { Icon } from "@iconify/react/dist/iconify.js";
import { error } from "@/lib/swal";
import dynamic from "next/dynamic";
import SuggestionModal from "@/components/modal/suggestions-modal";
import { formatDateToIndonesian } from "@/utils/globals";
import ApprovalHistoryModal from "@/components/modal/approval-history-modal";
const imageSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }),
description: z
.string()
.min(2, { message: "Narasi Penugasan harus lebih dari 2 karakter." }),
creatorName: z.string().min(1, { message: "Creator diperlukan" }),
// tags: z.string().min(1, { message: "Judul diperlukan" }),
});
type Category = {
id: string;
name: string;
};
type FileType = {
id: number;
url: string;
thumbnailFileUrl: string;
fileName: string;
};
type Detail = {
id: string;
title: string;
description: string;
slug: string;
category: {
id: number;
name: string;
};
categoryName: string;
creatorName: string;
thumbnailLink: string;
url: string;
tags: string;
statusName: string;
isPublish: boolean;
needApprovalFromLevel: number;
files: FileType[];
uploadedById: number;
};
const ViewEditor = dynamic(
() => {
return import("@/components/editor/view-editor");
},
{ ssr: false }
);
export default function FormVideoDetail() {
const MySwal = withReactContent(Swal);
const router = useRouter();
const userId = getCookiesDecrypt("uie");
const userLevelId = getCookiesDecrypt("ulie");
const roleId = getCookiesDecrypt("urie");
const [modalOpen, setModalOpen] = useState(false);
const { id } = useParams() as { id: string };
console.log(id);
const editor = useRef(null);
type ImageSchema = z.infer<typeof imageSchema>;
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const taskId = Cookies.get("taskId");
const scheduleId = Cookies.get("scheduleId");
const scheduleType = Cookies.get("scheduleType");
const [status, setStatus] = useState("");
const [categories, setCategories] = useState<Category[]>([]);
const [selectedCategory, setSelectedCategory] = useState<any>();
const [tags, setTags] = useState<any[]>([]);
const [detail, setDetail] = useState<any>();
const [refresh, setRefresh] = useState(false);
const [selectedPublishers, setSelectedPublishers] = useState<number[]>([]);
const [description, setDescription] = useState("");
const [main, setMain] = useState<any>([]);
const [detailVideo, setDetailVideo] = useState<any>([]);
const [thumbsSwiper, setThumbsSwiper] = useState<any>(null);
const [filePlacements, setFilePlacements] = useState<string[][]>([]);
const [selectedTarget, setSelectedTarget] = useState("");
const [files, setFiles] = useState<FileType[]>([]);
const [rejectedFiles, setRejectedFiles] = useState<number[]>([]);
const [isUserMabesApprover, setIsUserMabesApprover] = useState(false);
const [approval, setApproval] = useState<any>();
let fileTypeId = "2";
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<ImageSchema>({
resolver: zodResolver(imageSchema),
});
// const handleKeyDown = (e: any) => {
// const newTag = e.target.value.trim(); // Ambil nilai input
// if (e.key === "Enter" && newTag) {
// e.preventDefault(); // Hentikan submit form
// if (!tags.includes(newTag)) {
// setTags((prevTags) => [...prevTags, newTag]); // Tambah tag baru
// setValue("tags", ""); // Kosongkan input
// }
// }
// };
useEffect(() => {
if (
userLevelId != undefined &&
roleId != undefined &&
userLevelId == "216" &&
roleId == "3"
) {
setIsUserMabesApprover(true);
}
}, [userLevelId, roleId]);
const handleCheckboxChange = (id: number) => {
setSelectedPublishers((prev) =>
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]
);
};
useEffect(() => {
async function initState() {
getCategories();
}
initState();
}, []);
const getCategories = async () => {
try {
const category = await listEnableCategory(fileTypeId);
const resCategory: Category[] = category?.data?.data?.content;
setCategories(resCategory);
console.log("data category", resCategory);
if (scheduleId && scheduleType === "3") {
const findCategory = resCategory.find((o) =>
o.name.toLowerCase().includes("pers rilis")
);
if (findCategory) {
// setValue("categoryId", findCategory.id);
setSelectedCategory(findCategory.id);
const response = await getTagsBySubCategoryId(findCategory.id);
setTags(response?.data?.data);
}
}
} catch (error) {
console.error("Failed to fetch categories:", error);
}
};
useEffect(() => {
async function initState() {
if (id) {
const response = await detailMedia(id);
const details = response?.data?.data;
console.log("detail", details);
setFiles(details?.files);
setDetail(details);
setMain({
type: details?.fileType.name,
url: details?.files[0]?.url,
names: details?.files[0]?.fileName,
format: details?.files[0]?.format,
});
if (details?.publishedForObject) {
const publisherIds = details.publishedForObject.map(
(obj: any) => obj.id
);
setSelectedPublishers(publisherIds);
}
// Set the selected target to the category ID from details
setSelectedTarget(String(details.category.id));
const filesData = details?.files || [];
const fileUrls = filesData.map((files: { url: string }) =>
files.url ? files.url : "default-image.jpg"
);
setDetailVideo(fileUrls);
const approvals = await getDataApprovalByMediaUpload(details?.id);
setApproval(approvals?.data?.data);
}
}
initState();
}, [refresh, setValue]);
const actionApproval = (e: string) => {
const temp = [];
for (const element of detail.files) {
temp.push([]);
}
setFilePlacements(temp);
setStatus(e);
setFiles(detail.files);
setDescription("");
setModalOpen(true);
};
const submit = async () => {
if (
(description?.length > 1 && Number(status) == 3) ||
Number(status) == 2 ||
Number(status) == 4
) {
save();
// MySwal.fire({
// title: "Simpan Approval",
// text: "",
// icon: "warning",
// showCancelButton: true,
// cancelButtonColor: "#d33",
// confirmButtonColor: "#3085d6",
// confirmButtonText: "Simpan",
// }).then((result) => {
// if (result.isConfirmed) {
// }
// });
}
};
async function save() {
const data = {
mediaUploadId: id,
statusId: status,
message: description,
files: isUserMabesApprover ? getPlacement() : [],
};
setModalOpen(false);
loading();
const response = await submitApproval(data);
if (response?.error) {
error(response.message);
return false;
}
const dataReject = {
listFiles: rejectedFiles,
};
const resReject = await rejectFiles(dataReject);
if (resReject?.error) {
error(resReject.message);
return false;
}
close();
submitApprovalSuccesss();
return false;
}
const getPlacement = () => {
console.log("getPlaa", filePlacements);
const temp = [];
for (let i = 0; i < filePlacements?.length; i++) {
if (filePlacements[i].length !== 0) {
const now = filePlacements[i].filter((a) => a !== "all");
const data = { mediaFileId: files[i].id, placements: now.join(",") };
temp.push(data);
}
}
return temp;
};
const setupPlacement = (
index: number,
placement: string,
checked: boolean
) => {
let temp = [...filePlacements];
if (checked) {
if (placement === "all") {
temp[index] = ["all", "mabes", "polda", "international"];
} else {
const now = temp[index];
now.push(placement);
if (now.length === 3 && !now.includes("all")) {
now.push("all");
}
temp[index] = now;
}
} else {
if (placement === "all") {
temp[index] = [];
} else {
const now = temp[index].filter((a) => a !== placement);
console.log("now", now);
temp[index] = now;
if (now.length === 3 && now.includes("all")) {
const newData = now.filter((b) => b !== "all");
temp[index] = newData;
}
}
}
setFilePlacements(temp);
};
function handleDeleteFileApproval(id: number) {
const selectedFiles = files.filter((file) => file.id != id);
setFiles(selectedFiles);
const rejects = rejectedFiles;
rejects.push(id);
setRejectedFiles(rejects);
}
const handleMain = (
type: string,
url: string,
names: string,
format: string
) => {
console.log("Test 3 :", type, url, names, format);
setMain({
type,
url,
names,
format,
});
return false;
};
const submitApprovalSuccesss = () => {
MySwal.fire({
title: "Sukses",
text: "Data berhasil disimpan.",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
router.push("/contributor/content/video");
}
});
};
return (
<form>
{detail !== undefined ? (
<div className="flex flex-col lg:flex-row gap-10">
<Card className="w-full lg:w-8/12">
<div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">Form Video</p>
<div className="gap-5 mb-5">
{/* Input Title */}
<div className="space-y-2 py-3">
<Label>Title</Label>
<Controller
control={control}
name="title"
render={({ field }) => (
<Input
type="text"
value={detail?.title}
onChange={field.onChange}
placeholder="Enter Title"
/>
)}
/>
{errors.title?.message && (
<p className="text-red-400 text-sm">
{errors.title.message}
</p>
)}
</div>
<div className="flex items-center">
<div className="py-3 w-full space-y-2">
<Label>Category</Label>
<Select
disabled
value={String(detail?.category?.id)}
// onValueChange={(id) => {
// console.log("Selected Category:", id);
// setSelectedTarget(id);
// }}
>
<SelectTrigger>
<SelectValue placeholder="Pilih" />
</SelectTrigger>
<SelectContent>
{/* Show the category from details if it doesn't exist in categories list */}
{detail &&
!categories.find(
(cat) =>
String(cat.id) === String(detail.category.id)
) && (
<SelectItem
key={String(detail.category.id)}
value={String(detail.category.id)}
>
{detail.category.name}
</SelectItem>
)}
{categories.map((category) => (
<SelectItem
key={String(category.id)}
value={String(category.id)}
>
{category.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="py-3 space-y-2">
<Label>Description</Label>
<Controller
control={control}
name="description"
render={({ field: { onChange, value } }) => (
<ViewEditor initialData={detail?.htmlDescription} />
)}
/>
{errors.description?.message && (
<p className="text-red-400 text-sm">
{errors.description.message}
</p>
)}
</div>
<div className="space-y-2">
<Label className="text-xl "> File Media</Label>
<div className="w-full">
<Swiper
thumbs={{ swiper: thumbsSwiper }}
modules={[FreeMode, Navigation, Thumbs]}
navigation={false}
className="w-full"
>
{files?.map((data: any) => (
<SwiperSlide key={data.id}>
<video
className="object-fill h-full w-full"
src={data.secondaryUrl}
controls
title={`Video ${data.id}`}
/>
</SwiperSlide>
))}
</Swiper>
<div className="mt-2">
<Swiper
onSwiper={setThumbsSwiper}
slidesPerView={8}
spaceBetween={8}
pagination={{
clickable: true,
}}
modules={[Pagination, Thumbs]}
// className="mySwiper2"
>
{files?.map((data: any) => (
<SwiperSlide key={data.id}>
<video
className="object-cover h-[60px] w-[80px] cursor-pointer"
src={data.secondaryUrl}
muted
title={`Video ${data.id}`}
/>
</SwiperSlide>
))}
</Swiper>
</div>
</div>
</div>
</div>
</div>
</Card>
<div className="w-full lg:w-4/12">
<Card className="pb-3">
<div className="px-3 py-3">
<div className="space-y-2">
<Label>Creator</Label>
<Controller
control={control}
name="creatorName"
render={({ field }) => (
<Input
type="text"
value={detail?.creatorName}
onChange={field.onChange}
placeholder="Enter Title"
/>
)}
/>
{errors.creatorName?.message && (
<p className="text-red-400 text-sm">
{errors.creatorName.message}
</p>
)}
</div>
</div>
<div className="mt-3 px-3 space-y-2">
<Label>Preview</Label>
<Card className="mt-2">
<img
src={detail.thumbnailLink}
alt="Thumbnail Gambar Utama"
className="w-full h-auto rounded"
/>
</Card>
</div>
<div className="px-3 py-3">
<div className="space-y-2">
<Label>Tags</Label>
<div className="flex flex-wrap gap-2">
{detail?.tags
?.split(",")
.map((tag: string, index: number) => (
<Badge
key={index}
className="border rounded-md px-2 py-2"
>
{tag.trim()}
</Badge>
))}
</div>
</div>
</div>
<div className="px-3 py-3">
<div className="flex flex-col gap-6 space-y-2">
<Label>Publish Target</Label>
<div className="flex gap-2 items-center">
<Checkbox
id="5"
checked={selectedPublishers.includes(5)}
onChange={() => handleCheckboxChange(5)}
/>
<Label htmlFor="5">UMUM</Label>
</div>
<div className="flex gap-2 items-center">
<Checkbox
id="6"
checked={selectedPublishers.includes(6)}
onChange={() => handleCheckboxChange(6)}
/>
<Label htmlFor="6">JOURNALIS</Label>
</div>
<div className="flex gap-2 items-center">
<Checkbox
id="7"
checked={selectedPublishers.includes(7)}
onChange={() => handleCheckboxChange(7)}
/>
<Label htmlFor="7">POLRI</Label>
</div>
<div className="flex gap-2 items-center">
<Checkbox
id="8"
checked={selectedPublishers.includes(8)}
onChange={() => handleCheckboxChange(8)}
/>
<Label htmlFor="8">KSP</Label>
</div>
</div>
</div>
<SuggestionModal
id={Number(id)}
numberOfSuggestion={detail?.numberOfSuggestion}
/>
<div className="px-3 py-3 border mx-3">
<p>Information:</p>
<p className="text-sm text-slate-400">{detail?.statusName}</p>
<p>Komentar</p>
<p>{approval?.message}</p>
<p className="text-right text-sm">
{" "}
{approval?.approvalBy?.fullname} |{" "}
{formatDateToIndonesian(approval?.approvalDate)}
</p>
<ApprovalHistoryModal id={Number(id)} />
</div>
{/* {detail?.isPublish == false ? (
<div className="p-3">
<Button className="bg-blue-600">Publish</Button>
</div>
) : (
""
)} */}
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Leave Comment</DialogTitle>
</DialogHeader>
{status == "2"
? files?.map((file, index) => (
<div
key={file.id}
className="flex flex-row gap-2 items-center"
>
{/* <img src={file.url} className="w-[200px]" /> */}
<svg
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewBox="0 0 24 24"
>
<g fill="none" fill-rule="evenodd">
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
<path
fill="currentColor"
d="M20 3a2 2 0 0 1 1.995 1.85L22 5v14a2 2 0 0 1-1.85 1.995L20 21H4a2 2 0 0 1-1.995-1.85L2 19V5a2 2 0 0 1 1.85-1.995L4 3zm0 2H4v14h16zm-9.66 2.638l.518.23l.338.16l.387.19l.43.218l.47.25l.507.28l.266.152l.518.305l.474.292l.43.273l.38.253l.48.33l.364.263l.095.07a1.234 1.234 0 0 1 0 1.98l-.323.235l-.44.308l-.356.239l-.405.263l-.453.283l-.499.3l-.534.309l-.509.282l-.471.25l-.43.22l-.386.188l-.622.288l-.23.1a1.234 1.234 0 0 1-1.714-.99l-.058-.565l-.032-.374l-.042-.664l-.023-.508l-.015-.555l-.004-.294l-.002-.305q0-.31.006-.6l.015-.555l.023-.507l.027-.457l.03-.401l.075-.744a1.235 1.235 0 0 1 1.715-.992m.611 2.501l-.436-.218l-.029.487l-.022.551l-.013.61l-.002.325l.002.325l.013.609l.01.283l.026.52l.015.235l.434-.218l.487-.256l.535-.294l.284-.162l.551-.326l.494-.306l.436-.28l.196-.13l-.407-.27l-.466-.294a30 30 0 0 0-.803-.48l-.283-.161l-.534-.294z"
/>
</g>
</svg>
<div className="flex flex-col gap-2 w-full">
<div className="flex justify-between text-sm">
{file.fileName}
<a
onClick={() =>
handleDeleteFileApproval(file.id)
}
>
<Icon icon="humbleicons:times" color="red" />
</a>
</div>
{isUserMabesApprover && (
<div className="flex flex-row gap-2">
<div className="flex items-center space-x-2">
<Checkbox
id="terms"
value="all"
checked={filePlacements[index]?.includes(
"all"
)}
onCheckedChange={(e) =>
setupPlacement(index, "all", Boolean(e))
}
/>
<label
htmlFor="terms"
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
All
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="terms"
checked={filePlacements[index]?.includes(
"mabes"
)}
onCheckedChange={(e) =>
setupPlacement(index, "mabes", Boolean(e))
}
/>
<label
htmlFor="terms"
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Nasional
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="terms"
checked={filePlacements[index]?.includes(
"polda"
)}
onCheckedChange={(e) =>
setupPlacement(index, "polda", Boolean(e))
}
/>
<label
htmlFor="terms"
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Wilayah
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="terms"
checked={filePlacements[index]?.includes(
"international"
)}
onCheckedChange={(e) =>
setupPlacement(
index,
"international",
Boolean(e)
)
}
/>
<label
htmlFor="terms"
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Internasional
</label>
</div>
</div>
)}
</div>
</div>
))
: ""}
<div className="flex flex-col gap-4">
<Textarea
placeholder="Type your message here."
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</div>
{status == "3" || status == "4" ? (
<div className="flex flex-row gap-2">
<Badge
color={
description === "Kualitas media kurang baik"
? "primary"
: "default"
}
className="cursor-pointer"
onClick={() =>
setDescription("Kualitas media kurang baik")
}
>
Kualitas media kurang baik
</Badge>
<Badge
color={
description === "Deskripsi kurang lengkap"
? "primary"
: "default"
}
className="cursor-pointer"
onClick={() =>
setDescription("Deskripsi kurang lengkap")
}
>
Deskripsi kurang lengkap
</Badge>
<Badge
color={
description === "Judul kurang tepat"
? "primary"
: "default"
}
className="cursor-pointer"
onClick={() => setDescription("Judul kurang tepat")}
>
Judul kurang tepat
</Badge>
</div>
) : (
<div className="flex flex-row gap-2">
<Badge
color={
description === "Konten sangat bagus"
? "primary"
: "default"
}
className="cursor-pointer"
onClick={() => setDescription("Konten sangat bagus")}
>
Konten sangat bagus
</Badge>
<Badge
color={
description === "Konten menarik"
? "primary"
: "default"
}
className="cursor-pointer"
onClick={() => setDescription("Konten menarik")}
>
Konten menarik
</Badge>
</div>
)}
<DialogFooter>
<Button
type="button"
color="primary"
onClick={() => submit()}
>
Submit
</Button>
<Button
type="button"
color="destructive"
onClick={() => setModalOpen(false)}
>
Cancel
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Card>
{Number(detail?.needApprovalFromLevel) == Number(userLevelId) ? (
Number(detail?.uploadedById) == Number(userId) ? (
""
) : (
<div className="flex flex-col gap-2 p-3">
<Button
onClick={() => actionApproval("2")}
color="primary"
type="button"
>
<Icon icon="fa:check" className="mr-3" /> Accept
</Button>
<Button
onClick={() => actionApproval("3")}
className="bg-orange-400 hover:bg-orange-300"
type="button"
>
<Icon icon="fa:comment-o" className="mr-3" /> Revision
</Button>
<Button
onClick={() => actionApproval("4")}
color="destructive"
type="button"
>
<Icon icon="fa:times" className="mr-3" />
Reject
</Button>
</div>
)
) : (
""
)}
</div>
</div>
) : (
""
)}
</form>
);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,938 @@
"use client";
import React, { ChangeEvent, useEffect, useRef, useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Card } from "@/components/ui/card";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { useParams, useRouter } from "next/navigation";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { register } from "module";
import { Switch } from "@/components/ui/switch";
import Cookies from "js-cookie";
import {
createMedia,
getTagsBySubCategoryId,
listEnableCategory,
rejectFiles,
submitApproval,
} from "@/service/content/content";
import {
detailMedia,
getDataApprovalByMediaUpload,
} from "@/service/curated-content/curated-content";
import { Badge } from "@/components/ui/badge";
import { MailIcon, Music } from "lucide-react";
import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css";
import "swiper/css/free-mode";
import "swiper/css/navigation";
import "swiper/css/pagination";
import "swiper/css/thumbs";
import "swiper/css";
import "swiper/css/navigation";
import { FreeMode, Navigation, Pagination, Thumbs } from "swiper/modules";
import {
DialogHeader,
DialogFooter,
Dialog,
DialogContent,
DialogTitle,
} from "@/components/ui/dialog";
import { Textarea } from "@/components/ui/textarea";
import { loading } from "@/config/swal";
import { getCookiesDecrypt } from "@/lib/utils";
import { Icon } from "@iconify/react/dist/iconify.js";
import { error } from "@/lib/swal";
import dynamic from "next/dynamic";
import WaveSurfer from "wavesurfer.js";
import SuggestionModal from "@/components/modal/suggestions-modal";
import { formatDateToIndonesian } from "@/utils/globals";
import ApprovalHistoryModal from "@/components/modal/approval-history-modal";
import { useDropzone } from "react-dropzone";
import AudioPlayer from "@/components/audio-player";
const imageSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }),
description: z
.string()
.min(2, { message: "Narasi Penugasan harus lebih dari 2 karakter." }),
creatorName: z.string().min(1, { message: "Creator diperlukan" }),
// tags: z.string().min(1, { message: "Judul diperlukan" }),
});
type Category = {
id: string;
name: string;
};
type FileType = {
id: number;
secondaryUrl: string;
thumbnailFileUrl: string;
fileName: string;
};
type Detail = {
id: string;
title: string;
description: string;
slug: string;
category: {
id: number;
name: string;
};
categoryName: string;
creatorName: string;
thumbnailLink: string;
tags: string;
statusName: string;
isPublish: boolean;
needApprovalFromLevel: number;
files: FileType[];
uploadedById: number;
};
const ViewEditor = dynamic(
() => {
return import("@/components/editor/view-editor");
},
{ ssr: false }
);
export default function FormAudioDetail() {
const MySwal = withReactContent(Swal);
const router = useRouter();
const userId = getCookiesDecrypt("uie");
const userLevelId = getCookiesDecrypt("ulie");
const roleId = getCookiesDecrypt("urie");
const [modalOpen, setModalOpen] = useState(false);
const { id } = useParams() as { id: string };
console.log(id);
const editor = useRef(null);
type ImageSchema = z.infer<typeof imageSchema>;
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const taskId = Cookies.get("taskId");
const scheduleId = Cookies.get("scheduleId");
const scheduleType = Cookies.get("scheduleType");
const [status, setStatus] = useState("");
const [categories, setCategories] = useState<Category[]>([]);
const [selectedCategory, setSelectedCategory] = useState<any>();
const [tags, setTags] = useState<any[]>([]);
const [detail, setDetail] = useState<any>();
const [refresh, setRefresh] = useState(false);
const [selectedPublishers, setSelectedPublishers] = useState<number[]>([]);
const [description, setDescription] = useState("");
const [main, setMain] = useState<any>([]);
const [thumbsSwiper, setThumbsSwiper] = useState<any>(null);
const [selectedTarget, setSelectedTarget] = useState("");
const [rejectedFiles, setRejectedFiles] = useState<number[]>([]);
const [isUserMabesApprover, setIsUserMabesApprover] = useState(false);
const [audioPlaying, setAudioPlaying] = useState<any>(null);
const [filePlacements, setFilePlacements] = useState<string[][]>([]);
const [files, setFiles] = useState<FileType[]>([]);
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]);
const [detailThumb, setDetailThumb] = useState<string[]>([]);
const waveSurferRef = useRef<any>(null);
const waveSurfersRef = useRef<WaveSurfer[]>([]);
const [isPlayingIndex, setIsPlayingIndex] = useState<number | null>(null);
const [wavesurfer, setWavesurfer] = useState<WaveSurfer>();
const [isPlaying, setIsPlaying] = useState(false);
const [approval, setApproval] = useState<any>();
const onDrop = (acceptedFiles: File[]) => {
setUploadedFiles(acceptedFiles);
const blobUrls = acceptedFiles.map((file) => URL.createObjectURL(file));
setDetailThumb(blobUrls);
};
const onReady = (ws: WaveSurfer, index: number) => {
waveSurfersRef.current[index] = ws;
};
const onPlayPause = (index: number) => {
waveSurfersRef.current.forEach((ws, i) => {
if (i === index) {
ws.isPlaying() ? ws.pause() : ws.play();
setIsPlayingIndex(ws.isPlaying() ? index : null);
} else {
ws.pause();
}
});
};
const { getRootProps, getInputProps } = useDropzone({
onDrop,
accept: {
"audio/*": [],
},
multiple: true,
});
let fileTypeId = "4";
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<ImageSchema>({
resolver: zodResolver(imageSchema),
});
// const handleKeyDown = (e: any) => {
// const newTag = e.target.value.trim(); // Ambil nilai input
// if (e.key === "Enter" && newTag) {
// e.preventDefault(); // Hentikan submit form
// if (!tags.includes(newTag)) {
// setTags((prevTags) => [...prevTags, newTag]); // Tambah tag baru
// setValue("tags", ""); // Kosongkan input
// }
// }
// };
useEffect(() => {
if (
userLevelId != undefined &&
roleId != undefined &&
userLevelId == "216" &&
roleId == "3"
) {
setIsUserMabesApprover(true);
}
}, [userLevelId, roleId]);
const handleCheckboxChange = (id: number) => {
setSelectedPublishers((prev) =>
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]
);
};
useEffect(() => {
async function initState() {
getCategories();
}
initState();
}, []);
const getCategories = async () => {
try {
const category = await listEnableCategory(fileTypeId);
const resCategory: Category[] = category?.data.data.content;
setCategories(resCategory);
console.log("data category", resCategory);
if (scheduleId && scheduleType === "3") {
const findCategory = resCategory.find((o) =>
o.name.toLowerCase().includes("pers rilis")
);
if (findCategory) {
// setValue("categoryId", findCategory.id);
setSelectedCategory(findCategory.id);
const response = await getTagsBySubCategoryId(findCategory.id);
setTags(response?.data?.data);
}
}
} catch (error) {
console.error("Failed to fetch categories:", error);
}
};
const setupPlacementCheck = (length: number) => {
const temp = [];
for (let i = 0; i < length; i++) {
temp.push([]);
}
setFilePlacements(temp);
};
useEffect(() => {
async function initState() {
if (id) {
const response = await detailMedia(id);
const details = response?.data?.data;
console.log("detail", details);
setFiles(details?.files);
console.log("ISI FILES:", details?.files);
setDetail(details);
setMain({
type: details?.fileType.name,
url: details?.files[0]?.url,
names: details?.files[0]?.fileName,
format: details?.files[0]?.format,
});
setupPlacementCheck(details?.files?.length);
if (details?.publishedForObject) {
const publisherIds = details?.publishedForObject.map(
(obj: any) => obj.id
);
setSelectedPublishers(publisherIds);
}
setSelectedTarget(String(details.category.id));
const filesData = details?.files || [];
// const audioFiles = filesData.filter(
// (file: any) =>
// file.contentType &&
// (file.contentType.startsWith("audio/") ||
// file.contentType.includes("mpeg"))
// );
// const audioFiles = filesData.filter(
// (file: any) =>
// file.contentType && /^audio\/(mpeg|mp3|wav)$/.test(file.contentType)
// );
// const fileUrls = audioFiles.map((file: { secondaryUrl: string }) =>
// file.secondaryUrl ? file.secondaryUrl : ""
// );
// console.log("Audio file URLs:", fileUrls);
// setDetailThumb(fileUrls);
const approvals = await getDataApprovalByMediaUpload(details?.id);
setApproval(approvals?.data?.data);
}
}
initState();
}, [refresh, setValue]);
const actionApproval = (e: string) => {
const temp = [];
for (const element of detail.files) {
temp.push([]);
}
setFilePlacements(temp);
setStatus(e);
setFiles(detail.files);
setDescription("");
setModalOpen(true);
};
const submit = async () => {
if (
(description?.length > 1 && Number(status) == 3) ||
Number(status) == 2 ||
Number(status) == 4
) {
save();
// MySwal.fire({
// title: "Simpan Approval",
// text: "",
// icon: "warning",
// showCancelButton: true,
// cancelButtonColor: "#d33",
// confirmButtonColor: "#3085d6",
// confirmButtonText: "Simpan",
// }).then((result) => {
// if (result.isConfirmed) {
// }
// });
}
};
const getPlacement = () => {
console.log("getPlaa", filePlacements);
const temp = [];
for (let i = 0; i < filePlacements?.length; i++) {
if (filePlacements[i].length !== 0) {
const now = filePlacements[i].filter((a) => a !== "all");
const data = { mediaFileId: files[i].id, placements: now.join(",") };
temp.push(data);
}
}
return temp;
};
async function save() {
const data = {
mediaUploadId: id,
statusId: status,
message: description,
// files: [],
files: isUserMabesApprover ? getPlacement() : [],
};
setModalOpen(false);
loading();
const response = await submitApproval(data);
if (response?.error) {
error(response.message);
return false;
}
const dataReject = {
listFiles: rejectedFiles,
};
const resReject = await rejectFiles(dataReject);
if (resReject?.error) {
error(resReject.message);
return false;
}
close();
submitApprovalSuccesss();
return false;
}
function handleDeleteFileApproval(id: number) {
const selectedFiles = files.filter((file) => file.id != id);
setFiles(selectedFiles);
const rejects = rejectedFiles;
rejects.push(id);
setRejectedFiles(rejects);
}
const setupPlacement = (
index: number,
placement: string,
checked: boolean
) => {
let temp = [...filePlacements];
if (checked) {
if (placement === "all") {
temp[index] = ["all", "mabes", "polda", "international"];
} else {
const now = temp[index];
now.push(placement);
if (now.length === 3 && !now.includes("all")) {
now.push("all");
}
temp[index] = now;
}
} else {
if (placement === "all") {
temp[index] = [];
} else {
const now = temp[index].filter((a) => a !== placement);
console.log("now", now);
temp[index] = now;
if (now.length === 3 && now.includes("all")) {
const newData = now.filter((b) => b !== "all");
temp[index] = newData;
}
}
}
setFilePlacements(temp);
};
const handleMain = (
type: string,
url: string,
names: string,
format: string
) => {
console.log("Test 3 :", type, url, names, format);
setMain({
type,
url,
names,
format,
});
return false;
};
const handleAudioPlayPause = (audioSrc: string) => {
if (audioPlaying === audioSrc) {
setAudioPlaying(null);
} else {
setAudioPlaying(audioSrc);
}
};
const submitApprovalSuccesss = () => {
MySwal.fire({
title: "Sukses",
text: "Data berhasil disimpan.",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then(() => {
router.push("/in/contributor/content/audio");
});
};
return (
<form>
{detail !== undefined ? (
<div className="flex flex-col lg:flex-row gap-10">
<Card className="w-full lg:w-8/12">
<div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">Form Audio</p>
<div className="gap-5 mb-5">
{/* Input Title */}
<div className="space-y-2 py-3">
<Label>Title</Label>
<Controller
control={control}
name="title"
render={({ field }) => (
<Input
type="text"
value={detail?.title}
onChange={field.onChange}
placeholder="Enter Title"
/>
)}
/>
{errors.title?.message && (
<p className="text-red-400 text-sm">
{errors.title.message}
</p>
)}
</div>
<div className="flex items-center">
<div className="py-3 w-full space-y-2">
<Label>Category</Label>
<Select
disabled
value={String(detail?.category?.id)}
// onValueChange={(id) => {
// console.log("Selected Category:", id);
// setSelectedTarget(id);
// }}
>
<SelectTrigger>
<SelectValue placeholder="Pilih" />
</SelectTrigger>
<SelectContent>
{/* Show the category from details if it doesn't exist in categories list */}
{detail &&
!categories.find(
(cat) =>
String(cat.id) === String(detail.category.id)
) && (
<SelectItem
key={String(detail.category.id)}
value={String(detail.category.id)}
>
{detail.category.name}
</SelectItem>
)}
{categories.map((category) => (
<SelectItem
key={String(category.id)}
value={String(category.id)}
>
{category.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="py-3 space-y-2">
<Label>Description</Label>
<Controller
control={control}
name="description"
render={({ field: { onChange, value } }) => (
<ViewEditor initialData={detail?.htmlDescription} />
)}
/>
{errors.description?.message && (
<p className="text-red-400 text-sm">
{errors.description.message}
</p>
)}
</div>
<div className="w-full">
<Label className="text-xl space-y-2">File Media</Label>
<div className="w-full">
{files.length === 0 ? (
<p className="text-center text-gray-500">
Tidak ada file media
</p>
) : (
files.map((file, index) => (
<AudioPlayer
key={index}
urlAudio={file?.secondaryUrl}
fileName={file?.fileName}
/>
))
)}
</div>
</div>
</div>
</div>
</Card>
<div className="w-full lg:w-4/12">
<Card className="pb-3">
<div className="px-3 py-3">
<div className="space-y-2">
<Label>Creator</Label>
<Controller
control={control}
name="creatorName"
render={({ field }) => (
<Input
type="text"
value={detail?.creatorName}
onChange={field.onChange}
placeholder="Enter Title"
/>
)}
/>
{errors.creatorName?.message && (
<p className="text-red-400 text-sm">
{errors.creatorName.message}
</p>
)}
</div>
</div>
<div className="px-3 py-3">
<div className="space-y-2">
<Label>Tags</Label>
<div className="flex flex-wrap gap-2">
{detail?.tags
?.split(",")
.map((tag: string, index: number) => (
<Badge
key={index}
className="border rounded-md px-2 py-2"
>
{tag.trim()}
</Badge>
))}
</div>
</div>
</div>
<div className="px-3 py-3">
<div className="flex flex-col gap-6 space-y-2">
<Label>Publish Target</Label>
<div className="flex gap-2 items-center">
<Checkbox
id="5"
checked={selectedPublishers.includes(5)}
onChange={() => handleCheckboxChange(5)}
/>
<Label htmlFor="5">UMUM</Label>
</div>
<div className="flex gap-2 items-center">
<Checkbox
id="6"
checked={selectedPublishers.includes(6)}
onChange={() => handleCheckboxChange(6)}
/>
<Label htmlFor="6">JOURNALIS</Label>
</div>
<div className="flex gap-2 items-center">
<Checkbox
id="7"
checked={selectedPublishers.includes(7)}
onChange={() => handleCheckboxChange(7)}
/>
<Label htmlFor="7">POLRI</Label>
</div>
<div className="flex gap-2 items-center">
<Checkbox
id="8"
checked={selectedPublishers.includes(8)}
onChange={() => handleCheckboxChange(8)}
/>
<Label htmlFor="8">KSP</Label>
</div>
</div>
</div>
<SuggestionModal
id={Number(id)}
numberOfSuggestion={detail?.numberOfSuggestion}
/>
<div className="px-3 py-3 border mx-3">
<p>Information:</p>
<p className="text-sm text-slate-400">{detail?.statusName}</p>
<p>Komentar</p>
<p>{approval?.message}</p>
<p className="text-right text-sm">
{" "}
{approval?.approvalBy?.fullname} |{" "}
{formatDateToIndonesian(approval?.approvalDate)}
</p>
<ApprovalHistoryModal id={Number(id)} />
</div>
{/* {detail?.isPublish == false ? (
<div className="p-3">
<Button className="bg-blue-600">Publish</Button>
</div>
) : (
""
)} */}
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Leave Comment</DialogTitle>
</DialogHeader>
{status == "2"
? files?.map((file, index) => (
<div
key={file.id}
className="flex flex-row gap-2 items-center"
>
{/* <img src={file.url} className="w-[200px]" /> */}
<svg
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewBox="0 0 20 20"
>
<path
fill="currentColor"
d="M14.702 2.226A1 1 0 0 1 16 3.18v6.027a5.5 5.5 0 0 0-1-.184V6.18L8 8.368V15.5a2.5 2.5 0 1 1-1-2V5.368a1 1 0 0 1 .702-.955zM8 7.32l7-2.187V3.18L8 5.368zM5.5 14a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3m13.5.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0m-2.265-.436l-2.994-1.65a.5.5 0 0 0-.741.438v3.3a.5.5 0 0 0 .741.438l2.994-1.65a.5.5 0 0 0 0-.876"
/>
</svg>{" "}
<div className="flex flex-col gap-2 w-full">
<div className="flex justify-between text-sm">
{file.fileName}
<a
onClick={() =>
handleDeleteFileApproval(file.id)
}
>
<Icon icon="humbleicons:times" color="red" />
</a>
</div>
{isUserMabesApprover && (
<div className="flex flex-row gap-2">
<div className="flex items-center space-x-2">
<Checkbox
id="terms"
value="all"
checked={filePlacements[index]?.includes(
"all"
)}
onCheckedChange={(e) =>
setupPlacement(index, "all", Boolean(e))
}
/>
<label
htmlFor="terms"
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
All
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="terms"
checked={filePlacements[index]?.includes(
"mabes"
)}
onCheckedChange={(e) =>
setupPlacement(index, "mabes", Boolean(e))
}
/>
<label
htmlFor="terms"
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Nasional
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="terms"
checked={filePlacements[index]?.includes(
"polda"
)}
onCheckedChange={(e) =>
setupPlacement(index, "polda", Boolean(e))
}
/>
<label
htmlFor="terms"
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Wilayah
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="terms"
checked={filePlacements[index]?.includes(
"international"
)}
onCheckedChange={(e) =>
setupPlacement(
index,
"international",
Boolean(e)
)
}
/>
<label
htmlFor="terms"
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Internasional
</label>
</div>
</div>
)}
</div>
</div>
))
: ""}
<div className="flex flex-col gap-4">
<Textarea
placeholder="Type your message here."
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</div>
{status == "3" || status == "4" ? (
<div className="flex flex-row gap-2">
<Badge
color={
description === "Kualitas media kurang baik"
? "primary"
: "default"
}
className="cursor-pointer"
onClick={() =>
setDescription("Kualitas media kurang baik")
}
>
Kualitas media kurang baik
</Badge>
<Badge
color={
description === "Deskripsi kurang lengkap"
? "primary"
: "default"
}
className="cursor-pointer"
onClick={() =>
setDescription("Deskripsi kurang lengkap")
}
>
Deskripsi kurang lengkap
</Badge>
<Badge
color={
description === "Judul kurang tepat"
? "primary"
: "default"
}
className="cursor-pointer"
onClick={() => setDescription("Judul kurang tepat")}
>
Judul kurang tepat
</Badge>
</div>
) : (
<div className="flex flex-row gap-2">
<Badge
color={
description === "Konten sangat bagus"
? "primary"
: "default"
}
className="cursor-pointer"
onClick={() => setDescription("Konten sangat bagus")}
>
Konten sangat bagus
</Badge>
<Badge
color={
description === "Konten menarik"
? "primary"
: "default"
}
className="cursor-pointer"
onClick={() => setDescription("Konten menarik")}
>
Konten menarik
</Badge>
</div>
)}
<DialogFooter>
<Button
type="button"
color="primary"
onClick={() => submit()}
>
Submit
</Button>
<Button
type="button"
color="destructive"
onClick={() => {
setModalOpen(false);
}}
>
Cancel
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Card>
{Number(detail?.needApprovalFromLevel) == Number(userLevelId) ? (
Number(detail?.uploadedById) == Number(userId) ? (
""
) : (
<div className="flex flex-col gap-2 p-3">
<Button
onClick={() => actionApproval("2")}
color="primary"
type="button"
>
<Icon icon="fa:check" className="mr-3" /> Accept
</Button>
<Button
onClick={() => actionApproval("3")}
className="bg-orange-400 hover:bg-orange-300"
type="button"
>
<Icon icon="fa:comment-o" className="mr-3" /> Revision
</Button>
<Button
onClick={() => actionApproval("4")}
color="destructive"
type="button"
>
<Icon icon="fa:times" className="mr-3" />
Reject
</Button>
</div>
)
) : (
""
)}
</div>
</div>
) : (
""
)}
</form>
);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,907 @@
"use client";
import React, { ChangeEvent, useEffect, useRef, useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Card } from "@/components/ui/card";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { useParams, useRouter } from "next/navigation";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { register } from "module";
import { Switch } from "@/components/ui/switch";
import Cookies from "js-cookie";
import {
createMedia,
getTagsBySubCategoryId,
listEnableCategory,
rejectFiles,
submitApproval,
} from "@/service/content/content";
import {
detailMedia,
getDataApprovalByMediaUpload,
} from "@/service/curated-content/curated-content";
import { Badge } from "@/components/ui/badge";
import { MailIcon } from "lucide-react";
import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css";
import "swiper/css/free-mode";
import "swiper/css/navigation";
import "swiper/css/pagination";
import "swiper/css/thumbs";
import "swiper/css";
import "swiper/css/navigation";
import { FreeMode, Navigation, Pagination, Thumbs } from "swiper/modules";
import {
DialogHeader,
DialogFooter,
Dialog,
DialogContent,
DialogTitle,
} from "@/components/ui/dialog";
import { Textarea } from "@/components/ui/textarea";
import { loading } from "@/config/swal";
import { getCookiesDecrypt } from "@/lib/utils";
import { Icon } from "@iconify/react/dist/iconify.js";
import { error } from "@/lib/swal";
import dynamic from "next/dynamic";
import SuggestionModal from "@/components/modal/suggestions-modal";
import { formatDateToIndonesian } from "@/utils/globals";
import ApprovalHistoryModal from "@/components/modal/approval-history-modal";
import FileTextPreview from "../file-preview-text";
import FileTextThumbnail from "../file-text-thumbnail";
const imageSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }),
description: z
.string()
.min(2, { message: "Narasi Penugasan harus lebih dari 2 karakter." }),
creatorName: z.string().min(1, { message: "Creator diperlukan" }),
// tags: z.string().min(1, { message: "Judul diperlukan" }),
});
type Category = {
id: string;
name: string;
};
type FileType = {
id: number;
url: string;
thumbnailFileUrl: string;
fileName: string;
};
type Detail = {
id: string;
title: string;
description: string;
slug: string;
category: {
id: number;
name: string;
};
categoryName: string;
creatorName: string;
thumbnailLink: string;
tags: string;
statusName: string;
isPublish: boolean;
needApprovalFromLevel: number;
files: FileType[];
uploadedById: number;
};
const ViewEditor = dynamic(
() => {
return import("@/components/editor/view-editor");
},
{ ssr: false }
);
export default function FormTeksDetail() {
const MySwal = withReactContent(Swal);
const router = useRouter();
const userId = getCookiesDecrypt("uie");
const userLevelId = getCookiesDecrypt("ulie");
const roleId = getCookiesDecrypt("urie");
const [modalOpen, setModalOpen] = useState(false);
const { id } = useParams() as { id: string };
console.log(id);
const editor = useRef(null);
type ImageSchema = z.infer<typeof imageSchema>;
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const taskId = Cookies.get("taskId");
const scheduleId = Cookies.get("scheduleId");
const scheduleType = Cookies.get("scheduleType");
const [status, setStatus] = useState("");
const [categories, setCategories] = useState<Category[]>([]);
const [selectedCategory, setSelectedCategory] = useState<any>();
const [tags, setTags] = useState<any[]>([]);
const [detail, setDetail] = useState<any>();
const [refresh, setRefresh] = useState(false);
const [selectedPublishers, setSelectedPublishers] = useState<number[]>([]);
const [description, setDescription] = useState("");
const [main, setMain] = useState<any>([]);
const [detailThumb, setDetailThumb] = useState<any>([]);
const [thumbsSwiper, setThumbsSwiper] = useState<any>(null);
const [selectedTarget, setSelectedTarget] = useState("");
const [files, setFiles] = useState<FileType[]>([]);
const [rejectedFiles, setRejectedFiles] = useState<number[]>([]);
const [isMabesApprover, setIsMabesApprover] = useState(false);
const [filePlacements, setFilePlacements] = useState<string[][]>([]);
const [isUserMabesApprover, setIsUserMabesApprover] = useState(false);
const [approval, setApproval] = useState<any>();
let fileTypeId = "3";
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<ImageSchema>({
resolver: zodResolver(imageSchema),
});
// const handleKeyDown = (e: any) => {
// const newTag = e.target.value.trim(); // Ambil nilai input
// if (e.key === "Enter" && newTag) {
// e.preventDefault(); // Hentikan submit form
// if (!tags.includes(newTag)) {
// setTags((prevTags) => [...prevTags, newTag]); // Tambah tag baru
// setValue("tags", ""); // Kosongkan input
// }
// }
// };
useEffect(() => {
if (
userLevelId != undefined &&
roleId != undefined &&
userLevelId == "216" &&
roleId == "3"
) {
setIsUserMabesApprover(true);
}
}, [userLevelId, roleId]);
const handleCheckboxChange = (id: number) => {
setSelectedPublishers((prev) =>
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]
);
};
useEffect(() => {
async function initState() {
getCategories();
}
initState();
}, []);
const getCategories = async () => {
try {
const category = await listEnableCategory(fileTypeId);
const resCategory: Category[] = category?.data?.data?.content;
setCategories(resCategory);
console.log("data category", resCategory);
if (scheduleId && scheduleType === "3") {
const findCategory = resCategory.find((o) =>
o.name.toLowerCase().includes("pers rilis")
);
if (findCategory) {
// setValue("categoryId", findCategory.id);
setSelectedCategory(findCategory.id); // Set the selected category
const response = await getTagsBySubCategoryId(findCategory.id);
setTags(response?.data?.data);
}
}
} catch (error) {
console.error("Failed to fetch categories:", error);
}
};
const setupPlacementCheck = (length: number) => {
const temp = [];
for (let i = 0; i < length; i++) {
temp.push([]);
}
setFilePlacements(temp);
};
useEffect(() => {
async function initState() {
if (id) {
const response = await detailMedia(id);
const details = response?.data?.data;
console.log("detail", details);
setFiles(details?.files);
setDetail(details);
setMain({
type: details?.fileType.name,
url: details?.files[0]?.url,
names: details?.files[0]?.fileName,
format: details?.files[0]?.format,
});
if (details.publishedForObject) {
const publisherIds = details.publishedForObject.map(
(obj: any) => obj.id
);
setSelectedPublishers(publisherIds);
}
// Set the selected target to the category ID from details
setSelectedTarget(String(details.category.id));
const filesData = details.files || [];
const fileUrls = filesData.map((file: any) => ({
url: file.secondaryUrl || "default-image.jpg",
format: file.format,
fileName: file.fileName,
}));
setDetailThumb(fileUrls);
const approvals = await getDataApprovalByMediaUpload(details?.id);
setApproval(approvals?.data?.data);
}
}
initState();
}, [refresh, setValue]);
const actionApproval = (e: string) => {
const temp = [];
for (const element of detail.files) {
temp.push([]);
}
setFilePlacements(temp);
setStatus(e);
setFiles(detail.files);
setDescription("");
setModalOpen(true);
};
const submit = async () => {
if (
(description?.length > 1 && Number(status) == 3) ||
Number(status) == 2 ||
Number(status) == 4
) {
save();
// MySwal.fire({
// title: "Simpan Approval",
// text: "",
// icon: "warning",
// showCancelButton: true,
// cancelButtonColor: "#d33",
// confirmButtonColor: "#3085d6",
// confirmButtonText: "Simpan",
// }).then((result) => {
// if (result.isConfirmed) {
// }
// });
}
};
const getPlacement = () => {
console.log("getPlaa", filePlacements);
const temp = [];
for (let i = 0; i < filePlacements?.length; i++) {
if (filePlacements[i].length !== 0) {
const now = filePlacements[i].filter((a) => a !== "all");
const data = { mediaFileId: files[i].id, placements: now.join(",") };
temp.push(data);
}
}
return temp;
};
async function save() {
const data = {
mediaUploadId: id,
statusId: status,
message: description,
files: isUserMabesApprover ? getPlacement() : [],
};
setModalOpen(false);
loading();
const response = await submitApproval(data);
if (response?.error) {
error(response.message);
return false;
}
const dataReject = {
listFiles: rejectedFiles,
};
const resReject = await rejectFiles(dataReject);
if (resReject?.error) {
error(resReject.message);
return false;
}
close();
submitApprovalSuccesss();
return false;
}
const setupPlacement = (
index: number,
placement: string,
checked: boolean
) => {
let temp = [...filePlacements];
if (checked) {
if (placement === "all") {
temp[index] = ["all", "mabes", "polda", "international"];
} else {
const now = temp[index];
now.push(placement);
if (now.length === 3 && !now.includes("all")) {
now.push("all");
}
temp[index] = now;
}
} else {
if (placement === "all") {
temp[index] = [];
} else {
const now = temp[index].filter((a) => a !== placement);
console.log("now", now);
temp[index] = now;
if (now.length === 3 && now.includes("all")) {
const newData = now.filter((b) => b !== "all");
temp[index] = newData;
}
}
}
setFilePlacements(temp);
};
function handleDeleteFileApproval(id: number) {
const selectedFiles = files.filter((file) => file.id != id);
setFiles(selectedFiles);
const rejects = rejectedFiles;
rejects.push(id);
setRejectedFiles(rejects);
}
const handleMain = (
type: string,
url: string,
names: string,
format: string
) => {
console.log("Test 3 :", type, url, names, format);
setMain({
type,
url,
names,
format,
});
return false;
};
const submitApprovalSuccesss = () => {
MySwal.fire({
title: "Sukses",
text: "Data berhasil disimpan.",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then(() => {
router.push("/admin/content/document");
});
};
return (
<form>
{detail !== undefined ? (
<div className="flex flex-col lg:flex-row gap-10">
<Card className="w-full lg:w-8/12">
<div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">Form Text</p>
<div className="gap-5 mb-5">
{/* Input Title */}
<div className="space-y-2 py-3">
<Label>Title</Label>
<Controller
control={control}
name="title"
render={({ field }) => (
<Input
type="text"
value={detail?.title}
onChange={field.onChange}
placeholder="Enter Title"
/>
)}
/>
{errors.title?.message && (
<p className="text-red-400 text-sm">
{errors.title.message}
</p>
)}
</div>
<div className="flex items-center">
<div className="py-3 w-full space-y-2">
<Label>Category</Label>
<Select
disabled
value={String(detail?.category?.id)}
// onValueChange={(id) => {
// console.log("Selected Category:", id);
// setSelectedTarget(id);
// }}
>
<SelectTrigger>
<SelectValue placeholder="Pilih" />
</SelectTrigger>
<SelectContent>
{/* Show the category from details if it doesn't exist in categories list */}
{detail &&
!categories.find(
(cat) =>
String(cat.id) === String(detail.category.id)
) && (
<SelectItem
key={String(detail.category.id)}
value={String(detail.category.id)}
>
{detail.category.name}
</SelectItem>
)}
{categories.map((category) => (
<SelectItem
key={String(category.id)}
value={String(category.id)}
>
{category.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="py-3 space-y-2">
<Label>Description</Label>
<Controller
control={control}
name="description"
render={({ field: { onChange, value } }) => (
<ViewEditor initialData={detail?.htmlDescription} />
)}
/>
{errors.description?.message && (
<p className="text-red-400 text-sm">
{errors.description.message}
</p>
)}
</div>
<div className="space-y-2">
<Label className="text-xl">File Media </Label>
<div className="w-full">
<Swiper
thumbs={{ swiper: thumbsSwiper }}
modules={[FreeMode, Navigation, Thumbs]}
navigation={false}
className="w-full"
>
{detailThumb?.map((file: any, index: any) => (
<SwiperSlide key={index}>
<FileTextPreview file={file} />
</SwiperSlide>
))}
</Swiper>
<div className="mt-2">
<Swiper
onSwiper={setThumbsSwiper}
slidesPerView={8}
spaceBetween={8}
pagination={{ clickable: true }}
modules={[Pagination, Thumbs]}
>
{detailThumb?.map((file: any, index: any) => (
<SwiperSlide key={index}>
<FileTextThumbnail file={file} />
</SwiperSlide>
))}
</Swiper>
</div>
</div>
</div>
</div>
</div>
</Card>
<div className="w-full lg:w-4/12">
<Card className="pb-3">
<div className="px-3 py-3">
<div className="space-y-2">
<Label>Creator</Label>
<Controller
control={control}
name="creatorName"
render={({ field }) => (
<Input
type="text"
value={detail?.creatorName}
onChange={field.onChange}
placeholder="Enter Title"
/>
)}
/>
{errors.creatorName?.message && (
<p className="text-red-400 text-sm">
{errors.creatorName.message}
</p>
)}
</div>
</div>
{/* <div className="mt-3 px-3">
<Label>Pratinjau Gambar Utama</Label>
<Card className="mt-2">
<img
src={detail.thumbnailLink}
alt="Thumbnail Gambar Utama"
className="w-full h-auto rounded"
/>
</Card>
</div> */}
<div className="px-3 py-3">
<div className="space-y-2">
<Label>Tags</Label>
<div className="flex flex-wrap gap-2">
{detail?.tags
?.split(",")
.map((tag: string, index: number) => (
<Badge
key={index}
className="border rounded-md px-2 py-2"
>
{tag.trim()}
</Badge>
))}
</div>
</div>
</div>
<div className="px-3 py-3">
<div className="flex flex-col gap-6 space-y-2">
<Label>Publish Target</Label>
<div className="flex gap-2 items-center">
<Checkbox
id="5"
checked={selectedPublishers.includes(5)}
onChange={() => handleCheckboxChange(5)}
/>
<Label htmlFor="5">UMUM</Label>
</div>
<div className="flex gap-2 items-center">
<Checkbox
id="6"
checked={selectedPublishers.includes(6)}
onChange={() => handleCheckboxChange(6)}
/>
<Label htmlFor="6">JOURNALIS</Label>
</div>
<div className="flex gap-2 items-center">
<Checkbox
id="7"
checked={selectedPublishers.includes(7)}
onChange={() => handleCheckboxChange(7)}
/>
<Label htmlFor="7">POLRI</Label>
</div>
<div className="flex gap-2 items-center">
<Checkbox
id="8"
checked={selectedPublishers.includes(8)}
onChange={() => handleCheckboxChange(8)}
/>
<Label htmlFor="8">KSP</Label>
</div>
</div>
</div>
<SuggestionModal
id={Number(id)}
numberOfSuggestion={detail?.numberOfSuggestion}
/>
<div className="px-3 py-3 border mx-3">
<p>Information:</p>
<p className="text-sm text-slate-400">{detail?.statusName}</p>
<p>Komentar</p>
<p>{approval?.message}</p>
<p className="text-right text-sm">
{" "}
{approval?.approvalBy?.fullname} |{" "}
{formatDateToIndonesian(approval?.approvalDate)}
</p>
<ApprovalHistoryModal id={Number(id)} />
</div>
{/* {detail?.isPublish == false ? (
<div className="p-3">
<Button className="bg-blue-600">Publish</Button>
</div>
) : (
""
)} */}
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Leave Comment</DialogTitle>
</DialogHeader>
{status == "2"
? files?.map((file, index) => (
<div
key={file.id}
className="flex flex-row gap-2 items-center"
>
{/* <img src={file.url} className="w-[200px]" /> */}
<svg
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewBox="0 0 16 16"
>
<path
fill="currentColor"
d="M5 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V5.414a1.5 1.5 0 0 0-.44-1.06L9.647 1.439A1.5 1.5 0 0 0 8.586 1zM4 3a1 1 0 0 1 1-1h3v2.5A1.5 1.5 0 0 0 9.5 6H12v7a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1zm7.793 2H9.5a.5.5 0 0 1-.5-.5V2.207zM7 7.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5M7.5 9a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1zM7 11.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5M5.5 8a.5.5 0 1 0 0-1a.5.5 0 0 0 0 1M6 9.5a.5.5 0 1 1-1 0a.5.5 0 0 1 1 0M5.5 12a.5.5 0 1 0 0-1a.5.5 0 0 0 0 1"
/>
</svg>
<div className="flex flex-col gap-2 w-full">
<div className="flex justify-between text-sm">
{file.fileName}
<a
onClick={() =>
handleDeleteFileApproval(file.id)
}
>
<Icon icon="humbleicons:times" color="red" />
</a>
</div>
{isUserMabesApprover && (
<div className="flex flex-row gap-2">
<div className="flex items-center space-x-2">
<Checkbox
id="terms"
value="all"
checked={filePlacements[index]?.includes(
"all"
)}
onCheckedChange={(e) =>
setupPlacement(index, "all", Boolean(e))
}
/>
<label
htmlFor="terms"
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
All
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="terms"
checked={filePlacements[index]?.includes(
"mabes"
)}
onCheckedChange={(e) =>
setupPlacement(index, "mabes", Boolean(e))
}
/>
<label
htmlFor="terms"
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Nasional
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="terms"
checked={filePlacements[index]?.includes(
"polda"
)}
onCheckedChange={(e) =>
setupPlacement(index, "polda", Boolean(e))
}
/>
<label
htmlFor="terms"
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Wilayah
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="terms"
checked={filePlacements[index]?.includes(
"international"
)}
onCheckedChange={(e) =>
setupPlacement(
index,
"international",
Boolean(e)
)
}
/>
<label
htmlFor="terms"
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Internasional
</label>
</div>
</div>
)}
</div>
</div>
))
: ""}
<div className="flex flex-col gap-4">
<Textarea
placeholder="Type your message here."
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</div>
{status == "3" || status == "4" ? (
<div className="flex flex-row gap-2">
<Badge
color={
description === "Kualitas media kurang baik"
? "primary"
: "default"
}
className="cursor-pointer"
onClick={() =>
setDescription("Kualitas media kurang baik")
}
>
Kualitas media kurang baik
</Badge>
<Badge
color={
description === "Deskripsi kurang lengkap"
? "primary"
: "default"
}
className="cursor-pointer"
onClick={() =>
setDescription("Deskripsi kurang lengkap")
}
>
Deskripsi kurang lengkap
</Badge>
<Badge
color={
description === "Judul kurang tepat"
? "primary"
: "default"
}
className="cursor-pointer"
onClick={() => setDescription("Judul kurang tepat")}
>
Judul kurang tepat
</Badge>
</div>
) : (
<div className="flex flex-row gap-2">
<Badge
color={
description === "Konten sangat bagus"
? "primary"
: "default"
}
className="cursor-pointer"
onClick={() => setDescription("Konten sangat bagus")}
>
Konten sangat bagus
</Badge>
<Badge
color={
description === "Konten menarik"
? "primary"
: "default"
}
className="cursor-pointer"
onClick={() => setDescription("Konten menarik")}
>
Konten menarik
</Badge>
</div>
)}
<DialogFooter>
<Button
type="button"
color="primary"
onClick={() => submit()}
>
Submit
</Button>
<Button
type="button"
color="destructive"
onClick={() => {
setModalOpen(false);
}}
>
Cancel
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Card>
{Number(detail?.needApprovalFromLevel) == Number(userLevelId) ? (
Number(detail?.uploadedById) == Number(userId) ? (
""
) : (
<div className="flex flex-col gap-2 p-3">
<Button
onClick={() => actionApproval("2")}
color="primary"
type="button"
>
<Icon icon="fa:check" className="mr-3" /> Accept
</Button>
<Button
onClick={() => actionApproval("3")}
className="bg-orange-400 hover:bg-orange-300"
type="button"
>
<Icon icon="fa:comment-o" className="mr-3" /> Revision
</Button>
<Button
onClick={() => actionApproval("4")}
color="destructive"
type="button"
>
<Icon icon="fa:times" className="mr-3" />
Reject
</Button>
</div>
)
) : (
""
)}
</div>
</div>
) : (
""
)}
</form>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,976 @@
"use client";
import React, {
ChangeEvent,
Fragment,
useEffect,
useRef,
useState,
} from "react";
import { useForm, Controller } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Card } from "@/components/ui/card";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { useParams, useRouter } from "next/navigation";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { register } from "module";
import { Switch } from "@/components/ui/switch";
import Cookies from "js-cookie";
import {
createMedia,
getTagsBySubCategoryId,
listEnableCategory,
uploadThumbnail,
} from "@/service/content/content";
import { detailMedia } from "@/service/curated-content/curated-content";
import { Badge } from "@/components/ui/badge";
import { CloudUpload, MailIcon } from "lucide-react";
import { useDropzone } from "react-dropzone";
import { Icon } from "@iconify/react/dist/iconify.js";
import Image from "next/image";
import { error, loading } from "@/lib/swal";
import { Upload } from "tus-js-client";
import { getCsrfToken } from "@/service/auth";
import dynamic from "next/dynamic";
import { htmlToString } from "@/utils/globals";
import Link from "next/link";
const teksSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }),
description: z
.string()
.min(2, { message: "Narasi Penugasan harus lebih dari 2 karakter." }),
creatorName: z.string().min(1, { message: "Creator diperlukan" }),
// tags: z.string().min(1, { message: "Judul diperlukan" }),
});
type Category = {
id: string;
name: string;
};
type Detail = {
id: string;
title: string;
description: string;
slug: string;
categoryId: number;
category: {
id: number;
name: string;
};
publishedForObject: {
id: number;
name: string;
};
creatorName: string;
categoryName: string;
thumbnailLink: string;
tags: string;
};
interface FileWithPreview extends File {
preview: string;
}
type Option = {
id: string;
label: string;
};
const CustomEditor = dynamic(
() => {
return import("@/components/editor/custom-editor");
},
{ ssr: false }
);
export default function FormTeksUpdate() {
const MySwal = withReactContent(Swal);
const router = useRouter();
const { id } = useParams() as { id: string };
console.log(id);
const editor = useRef(null);
type TeksSchema = z.infer<typeof teksSchema>;
let progressInfo: any = [];
let counterUpdateProgress = 0;
const [progressList, setProgressList] = useState<any>([]);
let uploadPersen = 0;
const [isStartUpload, setIsStartUpload] = useState(false);
const [counterProgress, setCounterProgress] = useState(0);
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const taskId = Cookies.get("taskId");
const scheduleId = Cookies.get("scheduleId");
const scheduleType = Cookies.get("scheduleType");
const [categories, setCategories] = useState<Category[]>([]);
const [selectedCategory, setSelectedCategory] = useState<any>();
const [tags, setTags] = useState<any[]>([]);
const [detail, setDetail] = useState<Detail>();
const [refresh, setRefresh] = useState(false);
const [selectedPublishers, setSelectedPublishers] = useState<number[]>([]);
const [files, setFiles] = useState<FileWithPreview[]>([]);
const [selectedOptions, setSelectedOptions] = useState<{
[fileId: number]: string[];
}>({});
const [selectedTarget, setSelectedTarget] = useState("");
const [unitSelection, setUnitSelection] = useState({
allUnit: false,
mabes: false,
polda: false,
polres: false,
});
const [publishedFor, setPublishedFor] = useState<string[]>([]);
const inputRef = useRef<HTMLInputElement>(null);
let fileTypeId = "3";
const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) => {
setFiles(acceptedFiles.map((file) => Object.assign(file)));
},
accept: {
"application/pdf": [],
"application/msword": [], // .doc
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
[], // .docx
},
});
const options: Option[] = [
{ id: "all", label: "SEMUA" },
{ id: "5", label: "UMUM" },
{ id: "6", label: "JOURNALIS" },
{ id: "7", label: "POLRI" },
{ id: "8", label: "KSP" },
];
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<TeksSchema>({
resolver: zodResolver(teksSchema),
});
// const handleKeyDown = (e: any) => {
// const newTag = e.target.value.trim(); // Ambil nilai input
// if (e.key === "Enter" && newTag) {
// e.preventDefault(); // Hentikan submit form
// if (!tags.includes(newTag)) {
// setTags((prevTags) => [...prevTags, newTag]); // Tambah tag baru
// setValue("tags", ""); // Kosongkan input
// }
// }
// };
const handleImageChange = (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.files) {
const files = Array.from(event.target.files);
setSelectedFiles((prevImages: any) => [...prevImages, ...files]);
console.log("DATAFILE::", selectedFiles);
}
};
const handleRemoveImage = (index: number) => {
setSelectedFiles((prevImages) => prevImages.filter((_, i) => i !== index));
};
// const handleCheckboxChange = (id: number) => {
// setSelectedPublishers((prev) =>
// prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]
// );
// };
useEffect(() => {
async function initState() {
getCategories();
}
initState();
}, []);
const getCategories = async () => {
try {
const category = await listEnableCategory(fileTypeId);
const resCategory: Category[] = category?.data?.data?.content;
setCategories(resCategory);
console.log("data category", resCategory);
if (scheduleId && scheduleType === "3") {
const findCategory = resCategory.find((o) =>
o.name.toLowerCase().includes("pers rilis")
);
if (findCategory) {
// setValue("categoryId", findCategory.id);
setSelectedCategory(findCategory.id); // Set the selected category
const response = await getTagsBySubCategoryId(findCategory.id);
setTags(response?.data?.data);
}
}
} catch (error) {
console.error("Failed to fetch categories:", error);
}
};
useEffect(() => {
async function initState() {
if (id) {
const response = await detailMedia(id);
const details = response?.data?.data;
setDetail(details);
setSelectedTarget(String(details.category.id));
// Set form values immediately and then again after a delay to ensure editor is ready
setValue("title", details.title);
setValue("description", details.htmlDescription);
setValue("creatorName", details.creatorName);
// Set again after delay to ensure editor has loaded
setTimeout(() => {
setValue("title", details.title);
setValue("description", details.htmlDescription);
setValue("creatorName", details.creatorName);
}, 500);
if (details?.files) {
setFiles(details.files);
const initialOptions: { [key: number]: string[] } = {};
details.files.forEach((file: any) => {
if (file.placements) {
initialOptions[file.id] = mapPlacementsToOptions(file.placements);
}
});
setSelectedOptions(initialOptions);
}
if (details?.publishedFor) {
// Split string "7" to an array ["7"] if needed
setPublishedFor(details.publishedFor.split(","));
}
if (details?.tags) {
setTags(details.tags.split(",").map((tag: string) => tag.trim()));
}
// const matchingCategory = categories.find(
// (category) => category.id === details.categoryId
// );
// if (matchingCategory) {
// setSelectedTarget(matchingCategory.name);
// }
// setSelectedTarget(details.categoryId); // Untuk dropdown
}
}
initState();
}, [refresh, setValue]);
const mapPlacementsToOptions = (placements: string): string[] => {
const mapping: Record<string, string> = {
all: "all",
mabes: "nasional",
polda: "wilayah",
polres: "internasional",
};
// Jika placements hanya "all", langsung aktifkan semua checkbox
if (placements.trim() === "all") {
return ["all", "nasional", "wilayah", "internasional"];
}
const options = placements
.split(",")
.map((p) => mapping[p.trim()])
.filter(Boolean);
const allSelected = ["nasional", "wilayah", "internasional"].every((opt) =>
options.includes(opt)
);
return allSelected ? ["all", ...options] : options;
};
const handleCheckboxChange = (id: string) => {
if (id === "all") {
// Select all options except "all"
const allOptions = options
.filter((opt) => opt.id !== "all")
.map((opt) => opt.id);
setPublishedFor(
publishedFor.length === allOptions.length ? [] : allOptions
);
} else {
// Toggle individual option
setPublishedFor((prev) =>
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]
);
}
};
const save = async (data: TeksSchema) => {
loading();
const finalTags = tags.join(", ");
const requestData = {
...data,
id: detail?.id,
title: data.title,
description: htmlToString(data.description),
htmlDescription: data.description,
fileTypeId,
categoryId: selectedTarget,
subCategoryId: selectedTarget,
uploadedBy: "2b7c8d83-d298-4b19-9f74-b07924506b58",
statusId: "1",
publishedFor: publishedFor.join(","),
creatorName: data.creatorName,
tags: finalTags,
isYoutube: false,
isInternationalMedia: false,
};
const response = await createMedia(requestData);
console.log("Form Data Submitted:", requestData);
if (response?.error) {
error(response?.message);
return false;
}
const formMedia = new FormData();
const thumbnail = files[0];
formMedia.append("file", thumbnail);
const responseThumbnail = await uploadThumbnail(id, formMedia);
if (responseThumbnail?.error == true) {
error(responseThumbnail?.message);
return false;
}
const progressInfoArr = [];
for (const item of files) {
progressInfoArr.push({ percentage: 0, fileName: item.name });
}
progressInfo = progressInfoArr;
setIsStartUpload(true);
setProgressList(progressInfoArr);
close();
// showProgress();
files.map(async (item: any, index: number) => {
await uploadResumableFile(
index,
String(id),
item,
fileTypeId == "2" || fileTypeId == "4" ? item.duration : "0"
);
});
MySwal.fire({
title: "Sukses",
text: "Data berhasil disimpan.",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then(() => {
router.push("/admin/content/document");
});
};
async function uploadResumableFile(
idx: number,
id: string,
file: any,
duration: string
) {
console.log(idx, id, file, duration);
// const placements = getPlacement(file.placements);
// console.log("Placementttt: : ", placements);
const resCsrf = await getCsrfToken();
const csrfToken = resCsrf?.data?.token;
console.log("CSRF TOKEN : ", csrfToken);
const headers = {
"X-XSRF-TOKEN": csrfToken,
};
if (!file.secondaryUrl || file.secondaryUrl == "") {
const upload = new Upload(file, {
endpoint: `${process.env.NEXT_PUBLIC_API}/media/file/upload`,
headers: headers,
retryDelays: [0, 3000, 6000, 12_000, 24_000],
chunkSize: 20_000,
metadata: {
mediaid: id,
filename: file.name,
filetype: file.type,
duration,
isWatermark: "true", // hardcode
},
onBeforeRequest: function (req) {
var xhr = req.getUnderlyingObject();
xhr.withCredentials = true;
},
onError: async (e: any) => {
console.log("Error upload :", e);
error(e);
},
onChunkComplete: (
chunkSize: any,
bytesAccepted: any,
bytesTotal: any
) => {
const uploadPersen = Math.floor((bytesAccepted / bytesTotal) * 100);
progressInfo[idx].percentage = uploadPersen;
counterUpdateProgress++;
console.log(counterUpdateProgress);
setProgressList(progressInfo);
setCounterProgress(counterUpdateProgress);
},
onSuccess: async () => {
uploadPersen = 100;
progressInfo[idx].percentage = 100;
counterUpdateProgress++;
setCounterProgress(counterUpdateProgress);
successTodo();
},
});
upload.start();
}
}
const onSubmit = (data: TeksSchema) => {
MySwal.fire({
title: "Simpan Data",
text: "Apakah Anda yakin ingin menyimpan data ini?",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Simpan",
}).then((result) => {
if (result.isConfirmed) {
save(data);
}
});
};
const successSubmit = (redirect: string) => {
MySwal.fire({
title: "Sukses",
text: "Data berhasil disimpan.",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then(() => {
router.push(redirect);
});
};
function successTodo() {
let counter = 0;
for (const element of progressInfo) {
if (element.percentage == 100) {
counter++;
}
}
if (counter == progressInfo.length) {
setIsStartUpload(false);
// hideProgress();
Cookies.remove("idCreate");
successSubmit("/in/contributor/content/teks");
}
}
const handleRemoveAllFiles = () => {
setFiles([]);
};
const renderFilePreview = (file: FileWithPreview) => {
if (file?.type?.startsWith("image")) {
return (
<Image
width={48}
height={48}
alt={file.name}
src={URL.createObjectURL(file)}
className=" rounded border p-0.5"
/>
);
} else {
return <Icon icon="tabler:file-description" />;
}
};
const handleRemoveFile = (file: FileWithPreview) => {
const uploadedFiles = files;
const filtered = uploadedFiles.filter((i) => i.name !== file.name);
setFiles([...filtered]);
};
const fileList = files.map((file) => (
<div
key={file.name}
className=" flex justify-between border px-3.5 py-3 my-6 rounded-md"
>
<div className="flex gap-3 items-center">
<div className="file-preview">{renderFilePreview(file)}</div>
<div>
<div className=" text-sm text-card-foreground">{file.name}</div>
<div className=" text-xs font-light text-muted-foreground">
{Math.round(file.size / 100) / 10 > 1000 ? (
<>{(Math.round(file.size / 100) / 10000).toFixed(1)}</>
) : (
<>{(Math.round(file.size / 100) / 10).toFixed(1)}</>
)}
{" kb"}
</div>
</div>
</div>
<Button
type="button"
size="icon"
color="destructive"
variant="outline"
className=" border-none rounded-full"
onClick={() => handleRemoveFile(file)}
>
<Icon icon="tabler:x" className=" h-5 w-5" />
</Button>
</div>
));
const handleCheckboxChangeImage = (fileId: number, value: string) => {
setSelectedOptions((prev: any) => {
const currentSelections = prev[fileId] || [];
if (value === "all") {
// If "all" is clicked, toggle all options
if (currentSelections.includes("all")) {
return { ...prev, [fileId]: [] }; // Deselect all
}
return {
...prev,
[fileId]: ["all", "nasional", "wilayah", "internasional"],
}; // Select all
} else {
// If any other checkbox is clicked, toggle that checkbox
const updatedSelections = currentSelections.includes(value)
? currentSelections.filter((option: any) => option !== value)
: [...currentSelections, value];
// If all individual options are selected, include "all" automatically
const isAllSelected = ["nasional", "wilayah", "internasional"].every(
(opt) => updatedSelections.includes(opt)
);
return {
...prev,
[fileId]: isAllSelected
? ["all", ...updatedSelections]
: updatedSelections.filter((opt: any) => opt !== "all"),
};
}
});
};
const handleAddTag = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && e.currentTarget.value.trim()) {
e.preventDefault();
const newTag = e.currentTarget.value.trim();
if (!tags.includes(newTag)) {
setTags((prevTags) => [...prevTags, newTag]); // Tambahkan tag baru
if (inputRef.current) {
inputRef.current.value = ""; // Kosongkan input
}
}
}
};
const handleRemoveTag = (index: number) => {
setTags((prevTags) => prevTags.filter((_, i) => i !== index));
};
const handleEditTag = (index: number, newValue: string) => {
setTags((prevTags) =>
prevTags.map((tag, i) => (i === index ? newValue : tag))
);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{detail !== undefined ? (
<div className="flex flex-col lg:flex-row gap-10">
<Card className="w-full lg:w-8/12">
<div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">Form Text</p>
<div className="gap-5 mb-5">
{/* Input Title */}
<div className="space-y-2 py-3">
<Label>Title</Label>
<Controller
control={control}
name="title"
render={({ field }) => (
<Input
type="text"
value={field?.value}
onChange={field.onChange}
placeholder="Enter Title"
/>
)}
/>
{errors.title?.message && (
<p className="text-red-400 text-sm">
{errors.title.message}
</p>
)}
</div>
<div className="flex items-center">
<div className="py-3 w-full space-y-2">
<Label>Category</Label>
<Select
value={selectedTarget}
onValueChange={(id) => {
console.log("Selected Category:", id);
setSelectedTarget(id);
}}
>
<SelectTrigger>
<SelectValue placeholder="Pilih" />
</SelectTrigger>
<SelectContent>
{/* Show the category from details if it doesn't exist in categories list */}
{detail &&
!categories.find(
(cat) =>
String(cat.id) === String(detail.category.id)
) && (
<SelectItem
key={String(detail.category.id)}
value={String(detail.category.id)}
>
{detail.category.name}
</SelectItem>
)}
{categories.map((category) => (
<SelectItem
key={String(category.id)}
value={String(category.id)}
>
{" "}
{category.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="py-3 space-y-2">
<Label>Description</Label>
<Controller
control={control}
name="description"
render={({ field: { onChange, value } }) => (
<CustomEditor onChange={onChange} initialData={value} />
)}
/>
{errors.description?.message && (
<p className="text-red-400 text-sm">
{errors.description.message}
</p>
)}
</div>
<div className="py-3 space-y-2">
<Label>Select File</Label>
{/* <Input
id="fileInput"
type="file"
onChange={handleImageChange}
/> */}
<Fragment>
<div {...getRootProps({ className: "dropzone" })}>
<input {...getInputProps()} />
<div className=" w-full text-center border-dashed border border-default-200 dark:border-default-300 rounded-md py-[52px] flex items-center flex-col">
<CloudUpload className="text-default-300 w-10 h-10" />
<h4 className=" text-2xl font-medium mb-1 mt-3 text-card-foreground/80">
{/* Drop files here or click to upload. */}
Drag File
</h4>
<div className=" text-xs text-muted-foreground">
Upload File Text Max
</div>
</div>
</div>
{files.length ? (
<Fragment>
<div>{fileList}</div>
<div className=" flex justify-between gap-2">
<div className="flex flex-row items-center gap-3 py-3">
<Label>Watermark</Label>
<div className="flex items-center gap-3">
<Switch defaultChecked color="primary" id="c2" />
</div>
</div>
<Button
color="destructive"
onClick={handleRemoveAllFiles}
>
Remove All
</Button>
</div>
</Fragment>
) : null}
{files.length > 0 && (
<div className="mt-4 space-y-2">
<Label className="text-lg font-semibold">
{" "}
File Media
</Label>
<div className="grid gap-4">
{files.map((file: any) => (
<div
key={file.id}
className="flex items-center border p-2 rounded-md"
>
<img
src={file.thumbnailFileUrl}
alt={file.fileName}
className="w-16 h-16 object-cover rounded-md mr-4"
/>
<div className="flex flex-wrap gap-3 items-center ">
<div className="flex-grow">
<p className="font-medium">{file.fileName}</p>
<a
href={file.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 text-sm"
>
View File
</a>
</div>
<div>
<Label className="flex items-center space-x-2">
<input
type="checkbox"
checked={selectedOptions[
file.id
]?.includes("all")}
onChange={() =>
handleCheckboxChangeImage(
file.id,
"all"
)
}
className="form-checkbox"
/>
<span>All</span>
</Label>
</div>
<div>
<Label className="flex items-center space-x-2">
<input
type="checkbox"
checked={selectedOptions[
file.id
]?.includes("nasional")}
onChange={() =>
handleCheckboxChangeImage(
file.id,
"nasional"
)
}
className="form-checkbox"
/>
<span>Nasional</span>
</Label>
</div>
<div>
<Label className="flex items-center space-x-2">
<input
type="checkbox"
checked={selectedOptions[
file.id
]?.includes("wilayah")}
onChange={() =>
handleCheckboxChangeImage(
file.id,
"wilayah"
)
}
className="form-checkbox"
/>
<span>Wilayah</span>
</Label>
</div>
<div>
<Label className="flex items-center space-x-2">
<input
type="checkbox"
checked={selectedOptions[
file.id
]?.includes("internasional")}
onChange={() =>
handleCheckboxChangeImage(
file.id,
"internasional"
)
}
className="form-checkbox"
/>
<span>Internasional</span>
</Label>
</div>
</div>
</div>
))}
</div>
</div>
)}
</Fragment>
</div>
</div>
</div>
</Card>
<div className="w-full lg:w-4/12">
<Card className=" h-[800px]">
<div className="px-3 py-3">
<div className="space-y-2">
<Label>Creator</Label>
<Controller
control={control}
name="creatorName"
render={({ field }) => (
<Input
type="text"
value={field?.value}
onChange={field.onChange}
placeholder="Enter Title"
/>
)}
/>
{errors.creatorName?.message && (
<p className="text-red-400 text-sm">
{errors.creatorName.message}
</p>
)}
</div>
</div>
{/* <div className="mt-3 px-3">
<Label>Pratinjau Gambar Utama</Label>
<Card className="mt-2">
<img
src={detail.thumbnailLink}
alt="Thumbnail Gambar Utama"
className="w-full h-auto rounded"
/>
</Card>
</div> */}
<div className="px-3 py-3">
<div className="space-y-2">
<Label>Tags</Label>
<Input
type="text"
id="tags"
placeholder="Add a tag and press Enter"
onKeyDown={handleAddTag}
ref={inputRef}
/>
<div className="mt-3 flex flex-wrap gap-2">
{tags.map((tag: any, index: any) => (
<span
key={index}
className="flex items-center gap-2 px-2 py-1 rounded-lg bg-black text-white text-sm"
>
<input
type="text"
value={tag}
onChange={(e) => handleEditTag(index, e.target.value)}
className="bg-black text-white border-none focus:outline-none w-auto"
/>
<button
value={tag}
type="button"
onClick={() => handleRemoveTag(index)}
className="remove-tag-button text-white"
>
×
</button>
</span>
))}
</div>
</div>
</div>
<div className="px-3 py-3">
<div className="flex flex-col gap-6">
<Label>Publish Target</Label>
{options.map((option: any) => (
<div key={option.id} className="flex gap-2 items-center">
<Checkbox
id={option.id}
checked={
option.id === "all"
? publishedFor.length ===
options.filter((opt: any) => opt.id !== "all")
.length
: publishedFor.includes(option.id)
}
onCheckedChange={() => handleCheckboxChange(option.id)}
/>
<Label htmlFor={option.id}>{option.label}</Label>
</div>
))}
</div>
</div>
<div className="px-3 py-3 flex flex-row items-center text-blue-500 gap-2 text-sm">
<MailIcon />
<p className="">Suggestion Box (0)</p>
</div>
<div className="px-3 py-3">
<p>Information:</p>
{/* <p>{detail?.status}</p> */}
</div>
</Card>
<div className="flex flex-row justify-end gap-3">
<div className="mt-4">
<Button type="submit" color="primary">
Submit
</Button>
</div>
<div className="mt-4">
<Link href={"/admin/content/document"}>
<Button type="button" color="primary" variant="outline">
Cancel
</Button>
</Link>
</div>
</div>
</div>
</div>
) : (
""
)}
</form>
);
}

View File

@ -0,0 +1,59 @@
import React from "react";
type FileData = {
url: string;
format: string; // extension with dot, e.g. ".pdf"
fileName?: string;
};
interface FilePreviewProps {
file: FileData;
}
const FileTextPreview: React.FC<FilePreviewProps> = ({ file }) => {
const format = file.format.toLowerCase();
if ([".jpg", ".jpeg", ".png", ".webp"].includes(format)) {
return (
<img
className="object-fill h-full w-full rounded-md"
src={file.url}
alt={file.fileName || "File"}
/>
);
}
if (format === ".pdf") {
return (
<iframe
className="w-full h-96 rounded-md"
src={`https://drive.google.com/viewerng/viewer?embedded=true&url=${encodeURIComponent(file.url)}`}
title={file.fileName || "PDF File"}
/>
);
}
if ([".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx"].includes(format)) {
return (
<iframe
className="w-full h-96 rounded-md"
src={`https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(file.url)}`}
title={file.fileName || "Document"}
/>
);
}
// Fallback → unknown format
return (
<a
href={file.url}
target="_blank"
rel="noopener noreferrer"
className="block text-blue-500 underline"
>
View {file.fileName || "File"}
</a>
);
};
export default FileTextPreview;

View File

@ -0,0 +1,33 @@
import React from "react";
type FileData = {
url: string;
format: string;
fileName?: string;
};
interface FileThumbnailProps {
file: FileData;
}
const FileTextThumbnail: React.FC<FileThumbnailProps> = ({ file }) => {
const format = file.format.toLowerCase();
if ([".jpg", ".jpeg", ".png", ".webp"].includes(format)) {
return (
<img
className="object-cover h-[60px] w-[80px] rounded-md"
src={file.url}
alt={file.fileName}
/>
);
}
return (
<div className="h-[60px] w-[80px] flex items-center justify-center bg-gray-200 text-sm text-center text-gray-700 rounded-md">
{format.replace(".", "").toUpperCase()}
</div>
);
};
export default FileTextThumbnail;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,199 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { zodResolver } from "@hookform/resolvers/zod";
import { Form, useForm } from "react-hook-form";
import { z } from "zod";
import { useEffect, useState } from "react";
import { getUserLevelForAssignments } from "@/service/task";
import {
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/components/ui/form";
const FormSchema = z.object({
items: z.array(z.string()).refine((value) => value.some((item) => item), {
message: "Required",
}),
});
interface UnitType {
id: number;
name: string;
subDestination: { id: number; name: string }[] | null;
}
export function UnitMapping(props: {
unit: "Polda" | "Satker" | "Polres";
sendDataToParent: (data: string[]) => void;
isDetail: boolean;
initData?: string[];
}) {
const { unit, sendDataToParent, isDetail } = props;
const [unitList, setUnitList] = useState<UnitType[]>([]);
const [satkerList, setSatkerList] = useState<{ id: number; name: string }[]>(
[]
);
const [polresList, setPolresList] = useState<{ id: number; name: string }[]>(
[]
);
const [isOpen, setIsOpen] = useState(false);
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
items: props.initData ? props.initData : [],
},
});
useEffect(() => {
async function initState() {
const response = await getUserLevelForAssignments();
setupUnit(response?.data?.data.list);
console.log("list", response?.data?.data.list);
}
initState();
}, []);
const unitType = form.watch("items");
const isAllUnitChecked =
unitType && unitList.every((item) => unitType.includes(String(item.id)));
const isAllSatkerChecked =
unitType && satkerList.every((item) => unitType.includes(String(item.id)));
const isAllPolresChecked = polresList.every((item) =>
unitType?.includes(String(item.id))
);
const setupUnit = (data: UnitType[]) => {
const temp = data.filter((a) => a.name.includes("POLDA"));
const temp2 = data.filter((a) => a.name.includes("SATKER"));
const temp3 = temp.flatMap((item) => item.subDestination || []);
setUnitList(temp);
setSatkerList(temp2[0]?.subDestination || []);
setPolresList(temp3);
};
useEffect(() => {
sendDataToParent(form.getValues("items"));
}, [unitType]);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<a
onClick={() => setIsOpen(true)}
className="text-primary cursor-pointer text-xs mr-3"
>
Pilih {unit}
</a>
</DialogTrigger>
<DialogContent className="h-[500px] overflow-y-auto">
<DialogHeader>
<DialogTitle>{unit}</DialogTitle>
</DialogHeader>
<Form {...form}>
<form className="flex flex-col gap-2">
<div className="flex items-center gap-3">
<Checkbox
id={`all-${unit}`}
checked={
unit === "Polda"
? isAllUnitChecked
: unit === "Satker"
? isAllSatkerChecked
: isAllPolresChecked
}
onCheckedChange={(checked) => {
if (checked) {
form.setValue(
"items",
unit === "Polda"
? unitList.map((item) => String(item.id))
: unit === "Satker"
? satkerList.map((item) => String(item.id))
: polresList.map((item) => String(item.id))
);
} else {
form.setValue("items", []);
}
}}
/>
<label htmlFor="all" className="text-sm text-black uppercase">
SEMUA {unit}
</label>
</div>
<FormField
control={form.control}
name="items"
render={() => (
<FormItem
className={`grid grid-cols-${
unit === "Polda" ? "2" : unit === "Satker" ? "3" : "4"
}`}
>
{(unit === "Polda"
? unitList
: unit === "Satker"
? satkerList
: polresList
)?.map((item: any) => (
<FormField
key={item.id}
control={form.control}
name="items"
render={({ field }) => {
return (
<FormItem
key={String(item.id)}
className="flex flex-row items-center space-x-3 space-y-0"
>
<FormControl>
<Checkbox
checked={field.value?.includes(String(item.id))}
onCheckedChange={(checked) => {
return checked
? field.onChange([
...field.value,
String(item.id),
])
: field.onChange(
field.value?.filter(
(value) => value !== String(item.id)
)
);
}}
/>
</FormControl>
<p className="text-sm text-black">{item.name}</p>
</FormItem>
);
}}
/>
))}
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</DialogContent>
</Dialog>
);
}

446
components/form/login.tsx Normal file
View File

@ -0,0 +1,446 @@
"use client";
import React, { useState } from "react";
import Link from "next/link";
import Cookies from "js-cookie";
import { useRouter } from "next/navigation";
import withReactContent from "sweetalert2-react-content";
import { Input } from "../ui/input";
import { Label } from "../ui/label";
import Swal from "sweetalert2";
import { EyeFilledIcon, EyeSlashFilledIcon } from "../icons";
import { Button } from "../ui/button";
import { error, loading } from "@/lib/swal";
import { doLogin, getProfile } from "@/service/auth";
export default function Login() {
const router = useRouter();
const [isVisible, setIsVisible] = useState(false);
const [isVisibleSetup, setIsVisibleSetup] = useState([false, false]);
const [oldEmail, setOldEmail] = useState("");
const [newEmail, setNewEmail] = useState("");
const [passwordSetup, setPasswordSetup] = useState("");
const [confPasswordSetup, setConfPasswordSetup] = useState("");
const toggleVisibility = () => setIsVisible(!isVisible);
const [needOtp, setNeedOtp] = useState(false);
const [isFirstLogin, setFirstLogin] = useState(false);
const [otpValue, setOtpValue] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [accessData, setAccessData] = useState<any>();
const [profile, setProfile] = useState<any>();
const [isValidEmail, setIsValidEmail] = useState(false);
const [isResetPassword, setIsResetPassword] = useState(false);
const [checkUsernameValue, setCheckUsernameValue] = useState("");
const MySwal = withReactContent(Swal);
const setValUsername = (e: any) => {
const uname = e.replaceAll(/[^\w.-]/g, "");
setUsername(uname.toLowerCase());
};
const onSubmit = async () => {
const data = {
username: username,
password: password,
};
if (!username || !password) {
error("Username & Password Wajib Diisi !");
} else {
loading();
const response = await doLogin(data);
if (response?.error) {
error("Username / Password Tidak Sesuai");
} else {
const profile = await getProfile(response?.data?.data?.access_token);
const dateTime: any = new Date();
const newTime: any = dateTime.getTime() + 10 * 60 * 1000;
Cookies.set("access_token", response?.data?.data?.access_token, {
expires: 1,
});
Cookies.set("refresh_token", response?.data?.data?.refresh_token, {
expires: 1,
});
Cookies.set("time_refresh", newTime, {
expires: 1,
});
Cookies.set("is_first_login", "true", {
secure: true,
sameSite: "strict",
});
// await saveActivity(
// {
// activityTypeId: 1,
// url: "https://dev.mikulnews.com/auth",
// userId: profile?.data?.data?.id,
// },
// response?.data?.data?.access_token
// );
Cookies.set("profile_picture", profile?.data?.data?.profilePictureUrl, {
expires: 1,
});
Cookies.set("uie", profile?.data?.data?.id, {
expires: 1,
});
Cookies.set("ufne", profile?.data?.data?.fullname, {
expires: 1,
});
Cookies.set("ulie", profile?.data?.data?.userLevelGroup, {
expires: 1,
});
Cookies.set("username", profile?.data?.data?.username, {
expires: 1,
});
Cookies.set("urie", profile?.data?.data?.roleId, {
expires: 1,
});
Cookies.set("roleName", profile?.data?.data?.roleName, {
expires: 1,
});
Cookies.set("masterPoldaId", profile?.data?.data?.masterPoldaId, {
expires: 1,
});
Cookies.set("ulne", profile?.data?.data?.userLevelId, {
expires: 1,
});
Cookies.set("urce", profile?.data?.data?.roleCode, {
expires: 1,
});
Cookies.set("email", profile?.data?.data?.email, {
expires: 1,
});
router.push("/admin/dashboard");
Cookies.set("status", "login", {
expires: 1,
});
close();
}
}
};
return (
<div className="min-h-screen flex">
<div className="hidden lg:flex lg:w-1/2 bg-white relative overflow-hidden">
<div className="absolute inset-0 bg-white"></div>
<div className="relative z-10 flex items-center justify-center w-full p-12">
<div className="text-center">
<Link href={"/"}>
<div>
<img
src="/Group.png"
alt="Mikul News Logo"
className="max-w-2xl h-auto drop-shadow-lg"
/>
</div>
</Link>
<div className="mt-8 text-white/90">
<h2 className="text-2xl font-bold mb-2">Portal Jaecoo</h2>
<p className="text-sm opacity-80">Platform beyond classic</p>
</div>
</div>
</div>
<div className="absolute top-10 left-10 w-20 h-20 bg-white/10 rounded-full blur-xl"></div>
<div className="absolute bottom-20 right-20 w-32 h-32 bg-white/5 rounded-full blur-2xl"></div>
</div>
<div className="w-full lg:w-1/2 flex items-center justify-center p-8">
<div className="w-full max-w-md">
<div className="lg:hidden text-center mb-8">
<Link href={"/"}>
<img
src="/masjaecoonav.png"
alt="Mikul News Logo"
className="w-64 h-10 mx-auto"
/>
</Link>
</div>
{isFirstLogin ? (
<div className="bg-white rounded-2xl shadow-xl p-8 border border-gray-100">
<div className="text-center mb-8">
<div className="w-16 h-16 bg-orange-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg
className="w-8 h-8 text-orange-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">
Setup Akun
</h2>
<p className="text-gray-600">Lengkapi informasi email Anda</p>
</div>
<div className="space-y-6">
<div>
<Label
htmlFor="old-email"
className="text-sm font-medium text-gray-700 mb-2 block"
>
Email Lama
</Label>
<Input
id="old-email"
type="email"
required
placeholder="Masukkan email lama"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500 transition-colors"
value={oldEmail}
onChange={(e) => setOldEmail(e.target.value)}
/>
</div>
<div>
<Label
htmlFor="new-email"
className="text-sm font-medium text-gray-700 mb-2 block"
>
Email Baru
</Label>
<Input
id="new-email"
type="email"
required
placeholder="Masukkan email baru"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500 transition-colors"
value={newEmail}
onChange={(e) => setNewEmail(e.target.value)}
/>
</div>
<Button
size="lg"
className="w-full bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white font-semibold py-3 rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl"
// onClick={submitCheckEmail}
>
Submit
</Button>
</div>
</div>
) : needOtp ? (
<div className="bg-white rounded-2xl shadow-xl p-8 border border-gray-100">
<div className="text-center">
<div className="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg
className="w-8 h-8 text-blue-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">
Verifikasi OTP
</h2>
<p className="text-gray-600">
Masukkan kode OTP yang telah dikirim
</p>
</div>
</div>
) : isResetPassword ? (
<div className="bg-white rounded-2xl shadow-xl p-8 border border-gray-100">
<div className="text-center mb-8">
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg
className="w-8 h-8 text-red-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">
Reset Password
</h2>
<p className="text-gray-600">
Masukkan username untuk reset password
</p>
</div>
<div className="space-y-6">
<div>
<Label
htmlFor="reset-username"
className="text-sm font-medium text-gray-700 mb-2 block"
>
Username
</Label>
<Input
id="reset-username"
type="text"
required
placeholder="Masukkan username"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-red-500 transition-colors"
value={checkUsernameValue}
onChange={(e) =>
setCheckUsernameValue(e.target.value.trim())
}
onPaste={(e) =>
setCheckUsernameValue(e.currentTarget.value.trim())
}
onCopy={(e) =>
setCheckUsernameValue(e.currentTarget.value.trim())
}
/>
</div>
<Button
size="lg"
className="w-full bg-gradient-to-r from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 text-white font-semibold py-3 rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed"
// onClick={checkUsername}
disabled={checkUsernameValue === ""}
>
Check Username
</Button>
<div className="flex justify-between items-center pt-4 border-t border-gray-100">
<Link
href={`/`}
className="text-sm text-gray-600 hover:text-gray-900 transition-colors lg:hidden"
>
Beranda
</Link>
<button
className="text-sm text-red-600 hover:text-red-700 font-medium transition-colors"
onClick={() => setIsResetPassword(false)}
>
Kembali ke Login
</button>
</div>
</div>
</div>
) : (
<div className=" ">
<div className="text-center mb-8">
<div className=" w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-8">
<img
src="/logo-netidhub.png"
alt="netidhub Logo"
className="max-w-[150px] h-auto drop-shadow-lg"
/>
</div>
<h2 className="text-lg font-bold text-gray-900 mb-2 mt-5">
MENYATUKAN INDONESIA
</h2>
</div>
<div className="space-y-6">
<div>
<Label
htmlFor="username"
className="text-sm font-medium text-gray-700 mb-2 block"
>
Username
</Label>
<Input
id="username"
required
type="text"
placeholder="Masukkan username"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 transition-colors"
value={username}
onChange={(e) => setValUsername(e.target.value.trim())}
onPaste={(e) =>
setValUsername(e.currentTarget.value.trim())
}
onCopy={(e) => setValUsername(e.currentTarget.value.trim())}
/>
</div>
<div>
<Label
htmlFor="password"
className="text-sm font-medium text-gray-700 mb-2 block"
>
Password
</Label>
<div className="relative">
<Input
id="password"
required
type={isVisible ? "text" : "password"}
placeholder="Masukkan password"
className="w-full px-4 py-3 pr-12 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 transition-colors"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button
type="button"
onClick={toggleVisibility}
className="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600 focus:outline-none transition-colors"
>
{isVisible ? (
<EyeSlashFilledIcon className="w-5 h-5" />
) : (
<EyeFilledIcon className="w-5 h-5" />
)}
</button>
</div>
</div>
<p className="text-[#007AFF]">Lupa Kata Sandi?</p>
<Link href={"/admin/dashboard"}>
<Button
size="lg"
className="w-full bg-gradient-to-r from-gray-500 to-gray-600 hover:from-gray-600 hover:to-gray-700 text-white font-semibold py-3 rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl"
onClick={onSubmit}
>
Masuk ke Portal
</Button>
</Link>
<div className="flex justify-center items-center border-gray-100">
<button
className="text-sm text-black font-medium transition-colors text-center "
onClick={() => setIsResetPassword(true)}
>
Belum Punya akun?{" "}
<span className="text-[#007AFF]">Daftar</span>
</button>
</div>
</div>
{/* <div className="mt-8 p-4 bg-blue-50 rounded-lg border border-blue-100">
<div className="flex items-start">
<svg className="w-5 h-5 text-blue-600 mt-0.5 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="text-sm text-blue-800">
<p className="font-medium mb-1">Informasi Portal</p>
<p className="text-blue-700">Akses informasi terkini dan status permintaan informasi yang telah diajukan.</p>
</div>
</div>
</div> */}
</div>
)}
</div>
</div>
</div>
);
}

291
components/form/sign-up.tsx Normal file
View File

@ -0,0 +1,291 @@
"use client";
import React, { useState } from "react";
import Link from "next/link";
import Cookies from "js-cookie";
// import { close, error, loading } from "@/config/swal";
import { useRouter } from "next/navigation";
import withReactContent from "sweetalert2-react-content";
import { Input } from "../ui/input";
import { Button } from "../ui/button";
import { Label } from "../ui/label";
// import { saveActivity } from "@/service/activity-log";
import Swal from "sweetalert2";
import { error } from "console";
import { EyeFilledIcon, EyeSlashFilledIcon } from "../icons";
import { RadioGroup, RadioGroupItem } from "../ui/radio-group";
export default function SignUp() {
const router = useRouter();
const [isVisible, setIsVisible] = useState(false);
const [isVisibleSetup, setIsVisibleSetup] = useState([false, false]);
const [oldEmail, setOldEmail] = useState("");
const [newEmail, setNewEmail] = useState("");
const [passwordSetup, setPasswordSetup] = useState("");
const [confPasswordSetup, setConfPasswordSetup] = useState("");
const toggleVisibility = () => setIsVisible(!isVisible);
const [needOtp, setNeedOtp] = useState(false);
const [isFirstLogin, setFirstLogin] = useState(false);
const [otpValue, setOtpValue] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [accessData, setAccessData] = useState<any>();
const [profile, setProfile] = useState<any>();
const [isValidEmail, setIsValidEmail] = useState(false);
const [isResetPassword, setIsResetPassword] = useState(false);
const [checkUsernameValue, setCheckUsernameValue] = useState("");
const MySwal = withReactContent(Swal);
const setValUsername = (e: any) => {
const uname = e.replaceAll(/[^\w.-]/g, "");
setUsername(uname.toLowerCase());
};
const onSubmit = () => {
// Simpan userId dummy ke cookie
Cookies.set("userId", "3");
// (Opsional) Simpan juga data lainnya jika dibutuhkan
// Cookies.set("username", username);
// Redirect ke halaman dashboard atau homepage
router.push("/"); // Ganti dengan path sesuai kebutuhan
};
const [step, setStep] = useState<"login" | "otp">("login");
const [email, setEmail] = useState("");
const [role, setRole] = useState("umum");
const [otp, setOtp] = useState(["", "", "", "", "", ""]);
const [membership, setMembership] = useState("");
const [certNumber, setCertNumber] = useState("");
const [membershipType, setMembershipType] = useState("");
const [nrp, setNrp] = useState("");
const handleSendOtp = (e: React.FormEvent) => {
e.preventDefault();
// Kirim OTP ke email
setStep("otp");
};
const handleVerifyOtp = (e: React.FormEvent) => {
e.preventDefault();
const code = otp.join("");
// Verifikasi kode OTP
console.log("Kode OTP:", code);
};
const handleOtpChange = (index: number, value: string) => {
if (!/^[0-9]?$/.test(value)) return;
const newOtp = [...otp];
newOtp[index] = value;
setOtp(newOtp);
// Fokus otomatis ke input selanjutnya
const nextInput = document.getElementById(`otp-${index + 1}`);
if (value && nextInput) nextInput.focus();
};
return (
<div className="min-h-screen flex">
{/* Left Side - Logo Section */}
<div className="hidden lg:flex lg:w-1/2 bg-white relative overflow-hidden">
<div className="absolute inset-0 bg-white"></div>
<div className="relative z-10 flex items-center justify-center w-full p-12">
<div className="text-center">
<Link href={"/"}>
<div>
<img
src="/Group.png"
alt="Mikul News Logo"
className="max-w-2xl h-auto drop-shadow-lg"
/>
</div>
</Link>
<div className="mt-8 text-white/90">
<h2 className="text-2xl font-bold mb-2">Portal NetIdhub</h2>
<p className="text-sm opacity-80">Platform beyond classic</p>
</div>
</div>
</div>
{/* Decorative elements */}
<div className="absolute top-10 left-10 w-20 h-20 bg-white/10 rounded-full blur-xl"></div>
<div className="absolute bottom-20 right-20 w-32 h-32 bg-white/5 rounded-full blur-2xl"></div>
</div>
{/* Right Side - Login Form */}
<div className="w-full lg:w-1/2 flex items-center justify-center p-8">
<div className="w-full max-w-md">
<div className=" ">
<div className="text-center mb-8">
<div className=" w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-8">
<img
src="/logo-netidhub.png"
alt="netidhub Logo"
className="max-w-[150px] h-auto drop-shadow-lg"
/>
</div>
<h2 className="text-lg font-bold text-gray-900 mb-2 mt-5">
MENYATUKAN INDONESIA
</h2>
</div>
{step === "login" ? (
<form
onSubmit={handleSendOtp}
className="w-full max-w-sm p-6 ml-0 md:ml-5"
>
{/* Radio Buttons */}
<RadioGroup
defaultValue="umum"
className="grid grid-cols-2 sm:grid-cols-4 gap-2 mb-6"
onValueChange={(val) => setRole(val)}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="umum" id="umum" />
<Label htmlFor="umum">Umum</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="jurnalis" id="jurnalis" />
<Label htmlFor="jurnalis">Jurnalis</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="polri" id="polri" />
<Label htmlFor="polri">POLRI</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="kpu" id="kpu" />
<Label htmlFor="kpu">KPU</Label>
</div>
</RadioGroup>
{/* Jurnalis: Select Keanggotaan */}
{role === "jurnalis" && (
<div className="mb-4 space-y-4">
<select
required
className="w-full border border-gray-300 rounded-md p-2 text-sm"
value={membershipType}
onChange={(e) => setMembershipType(e.target.value)}
>
<option value="">Pilih jenis keanggotaan</option>
<option value="pwi">
PWI (Persatuan Wartawan Indonesia)
</option>
<option value="ijti">
IJTI (Ikatan Jurnalis Televisi Indonesia)
</option>
<option value="pfi">PFI (Pewarta Foto Indonesia)</option>
<option value="aji">
AJI (Asosiasi Jurnalis Indonesia)
</option>
<option value="lainnya">Identitas Lainnya</option>
</select>
{/* Nomor Sertifikasi */}
<Input
type="text"
required
placeholder="Nomor Sertifikasi Wartawan"
value={certNumber}
onChange={(e) => setCertNumber(e.target.value)}
/>
</div>
)}
{/* Polri: NRP */}
{role === "polri" && (
<div className="mb-4">
<Input
type="text"
required
placeholder="NRP (Nomor Registrasi POLRI)"
value={nrp}
onChange={(e) => setNrp(e.target.value)}
/>
</div>
)}
{/* Email Field (Selalu Ada, tapi posisi bergantung role) */}
<Input
type="email"
required
placeholder="Email"
className="mb-4"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
{/* Note */}
<p className="text-xs text-gray-500 mb-4">
Dengan mendaftar, saya telah menyetujui{" "}
<a href="#" className="text-blue-600 underline">
Syarat dan Ketentuan
</a>{" "}
serta{" "}
<a href="#" className="text-blue-600 underline">
Kebijakan Privasi
</a>
</p>
{/* Submit */}
<Button
type="submit"
className="w-full bg-[#B89445] hover:bg-[#a1813d] text-white py-2 rounded-md text-base font-semibold"
>
Kirim OTP
</Button>
{/* Link Login */}
<p className="text-center text-sm mt-4">
Sudah punya akun?{" "}
<a href="/login" className="text-[#007AFF] hover:underline">
Login
</a>
</p>
</form>
) : (
<form
onSubmit={handleVerifyOtp}
className="w-full max-w-sm p-6 text-center ml-5 space-y-4"
>
<h3 className="text-base font-semibold text-gray-800">
Masukkan Kode OTP
</h3>
<p className="text-sm text-gray-500">
Silahkan cek inbox atau kotak spam pada email Anda.
</p>
<div className="flex justify-between mb-4">
{otp.map((value, index) => (
<input
key={index}
id={`otp-${index}`}
type="text"
inputMode="numeric"
maxLength={1}
value={value}
onChange={(e) => handleOtpChange(index, e.target.value)}
className="w-10 h-12 text-center border border-gray-300 rounded-md text-lg focus:outline-none focus:ring-2 focus:ring-[#B89445]"
/>
))}
</div>
<Button
type="submit"
className="w-full bg-[#B89445] hover:bg-[#a1813d] text-white py-2 rounded-md text-base font-semibold"
>
Lanjut
</Button>
<p className="text-sm text-black mt-4 cursor-pointer hover:underline">
Kirim Ulang OTP
</p>
</form>
)}
</div>
</div>
</div>
</div>
);
}

2738
components/icons.tsx Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,214 @@
import * as React from "react";
import { IconSvgProps } from "@/types/globals";
export const DashboardUserIcon = ({
size,
height = 48,
width = 48,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 3c2.21 0 4 1.79 4 4s-1.79 4-4 4s-4-1.79-4-4s1.79-4 4-4m4 10.54c0 1.06-.28 3.53-2.19 6.29L13 15l.94-1.88c-.62-.07-1.27-.12-1.94-.12s-1.32.05-1.94.12L11 15l-.81 4.83C8.28 17.07 8 14.6 8 13.54c-2.39.7-4 1.96-4 3.46v4h16v-4c0-1.5-1.6-2.76-4-3.46"
/>
</svg>
);
export const DashboardBriefcaseIcon = ({
size,
height = 48,
width = 48,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 48 48"
width={size || width}
{...props}
>
<path fill="#f5bc00" d="M44,41H4V10h40V41z" />
<polygon fill="#eb7900" points="44,26 24,26 4,26 4,10 44,10" />
<path fill="#eb7900" d="M17,26h-6v3h6V26z" />
<path fill="#eb7900" d="M37,26h-6v3h6V26z" />
<rect width="14" height="3" x="17" y="7" fill="#f5bc00" />
<path fill="#eb0000" d="M17,23h-6v3h6V23z" />
<path fill="#eb0000" d="M37,23h-6v3h6V23z" />
</svg>
);
export const DashboardMailboxIcon = ({
size,
height = 48,
width = 48,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
width={size || width}
{...props}
viewBox="0 0 48 48"
>
<path fill="#3dd9eb" d="M43,36H13V11h22c4.418,0,8,3.582,8,8V36z" />
<path
fill="#7debf5"
d="M21,36H5V19c0-4.418,3.582-8,8-8l0,0c4.418,0,8,3.582,8,8V36z"
/>
<path fill="#6c19ff" d="M21,36h5v8h-5V36z" />
<polygon fill="#eb0000" points="27,16 27,20 35,20 35,24 39,24 39,16" />
<rect width="8" height="3" x="9" y="20" fill="#3dd9eb" />
</svg>
);
export const DashboardShareIcon = ({
size,
height = 48,
width = 48,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
height={size || height}
width={size || width}
{...props}
viewBox="0 0 512 512"
>
<path
fill="currentColor"
d="M503.691 189.836L327.687 37.851C312.281 24.546 288 35.347 288 56.015v80.053C127.371 137.907 0 170.1 0 322.326c0 61.441 39.581 122.309 83.333 154.132c13.653 9.931 33.111-2.533 28.077-18.631C66.066 312.814 132.917 274.316 288 272.085V360c0 20.7 24.3 31.453 39.687 18.164l176.004-152c11.071-9.562 11.086-26.753 0-36.328"
/>
</svg>
);
export const DashboardSpeecIcon = ({
size,
height = 48,
width = 48,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
height={size || height}
width={size || width}
{...props}
viewBox="0 0 20 20"
>
<path
fill="currentColor"
d="M7 0a2 2 0 0 0-2 2h9a2 2 0 0 1 2 2v12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z"
/>
<path
fill="currentColor"
d="M13 20a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2zM9 5h4v5H9zM4 5h4v1H4zm0 2h4v1H4zm0 2h4v1H4zm0 2h9v1H4zm0 2h9v1H4zm0 2h9v1H4z"
/>
</svg>
);
export const DashboardConnectIcon = ({
size,
height = 48,
width = 48,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
height={size || height}
width={size || width}
{...props}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M2 22V4q0-.825.588-1.412T4 2h16q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18H6zm7.075-7.75L12 12.475l2.925 1.775l-.775-3.325l2.6-2.25l-3.425-.275L12 5.25L10.675 8.4l-3.425.275l2.6 2.25z"
/>
</svg>
);
export const DashboardTopLeftPointIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M18 18L6 6m0 0h9M6 6v9"
/>
</svg>
);
export const DashboardRightDownPointIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<path
fill="currentColor"
fillRule="evenodd"
d="M5.47 5.47a.75.75 0 0 1 1.06 0l10.72 10.72V9a.75.75 0 0 1 1.5 0v9a.75.75 0 0 1-.75.75H9a.75.75 0 0 1 0-1.5h7.19L5.47 6.53a.75.75 0 0 1 0-1.06"
clipRule="evenodd"
/>
</svg>
);
export const DashboardCommentIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
height={size || height}
width={size || width}
{...props}
viewBox="0 0 48 48"
>
<defs>
<mask id="ipSComment0">
<g
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="4"
>
<path fill="#fff" stroke="#fff" d="M44 6H4v30h9v5l10-5h21z" />
<path stroke="#000" d="M14 19.5v3m10-3v3m10-3v3" />
</g>
</mask>
</defs>
<path fill="currentColor" d="M0 0h48v48H0z" mask="url(#ipSComment0)" />
</svg>
);

View File

@ -0,0 +1,196 @@
import * as React from "react";
import { IconSvgProps } from "@/types/globals";
export const PdfIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
viewBox="0 0 15 15"
{...props}
>
<path
fill="currentColor"
d="M3.5 8H3V7h.5a.5.5 0 0 1 0 1M7 10V7h.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5z"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M1 1.5A1.5 1.5 0 0 1 2.5 0h8.207L14 3.293V13.5a1.5 1.5 0 0 1-1.5 1.5h-10A1.5 1.5 0 0 1 1 13.5zM3.5 6H2v5h1V9h.5a1.5 1.5 0 1 0 0-3m4 0H6v5h1.5A1.5 1.5 0 0 0 9 9.5v-2A1.5 1.5 0 0 0 7.5 6m2.5 5V6h3v1h-2v1h1v1h-1v2z"
clipRule="evenodd"
/>
</svg>
);
export const CsvIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 15 15"
>
<path
fill="currentColor"
fillRule="evenodd"
d="M1 1.5A1.5 1.5 0 0 1 2.5 0h8.207L14 3.293V13.5a1.5 1.5 0 0 1-1.5 1.5h-10A1.5 1.5 0 0 1 1 13.5zM2 6h3v1H3v3h2v1H2zm7 0H6v3h2v1H6v1h3V8H7V7h2zm2 0h-1v3.707l1.5 1.5l1.5-1.5V6h-1v3.293l-.5.5l-.5-.5z"
clipRule="evenodd"
/>
</svg>
);
export const ExcelIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 15 15"
width={size || width}
height={size || height}
{...props}
>
<path
fill="currentColor"
d="M3.793 7.5L2.146 5.854l.708-.708L4.5 6.793l1.646-1.647l.708.708L5.207 7.5l1.647 1.646l-.708.708L4.5 8.207L2.854 9.854l-.708-.708z"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M3.5 0A1.5 1.5 0 0 0 2 1.5V3h-.5A1.5 1.5 0 0 0 0 4.5v6A1.5 1.5 0 0 0 1.5 12H2v1.5A1.5 1.5 0 0 0 3.5 15h10a1.5 1.5 0 0 0 1.5-1.5v-12A1.5 1.5 0 0 0 13.5 0zm-2 4a.5.5 0 0 0-.5.5v6a.5.5 0 0 0 .5.5h6a.5.5 0 0 0 .5-.5v-6a.5.5 0 0 0-.5-.5z"
clipRule="evenodd"
/>
</svg>
);
export const WordIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 15 15"
>
<path
fill="currentColor"
d="m2.015 5.621l1 4a.5.5 0 0 0 .901.156l.584-.876l.584.876a.5.5 0 0 0 .901-.156l1-4l-.97-.242l-.726 2.903l-.373-.56a.5.5 0 0 0-.832 0l-.373.56l-.726-2.903z"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M3.5 0A1.5 1.5 0 0 0 2 1.5V3h-.5A1.5 1.5 0 0 0 0 4.5v6A1.5 1.5 0 0 0 1.5 12H2v1.5A1.5 1.5 0 0 0 3.5 15h10a1.5 1.5 0 0 0 1.5-1.5v-12A1.5 1.5 0 0 0 13.5 0zm-2 4a.5.5 0 0 0-.5.5v6a.5.5 0 0 0 .5.5h6a.5.5 0 0 0 .5-.5v-6a.5.5 0 0 0-.5-.5z"
clipRule="evenodd"
/>
</svg>
);
export const PptIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 15 15"
>
<path
fill="currentColor"
d="M3 8h.5a.5.5 0 0 0 0-1H3zm4 0h.5a.5.5 0 0 0 0-1H7z"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M1 1.5A1.5 1.5 0 0 1 2.5 0h8.207L14 3.293V13.5a1.5 1.5 0 0 1-1.5 1.5h-10A1.5 1.5 0 0 1 1 13.5zM2 6h1.5a1.5 1.5 0 1 1 0 3H3v2H2zm4 0h1.5a1.5 1.5 0 1 1 0 3H7v2H6zm5 5h1V7h1V6h-3v1h1z"
clipRule="evenodd"
/>
</svg>
);
export const FileIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 15 15"
>
<path
fill="currentColor"
d="m10.5.5l.354-.354L10.707 0H10.5zm3 3h.5v-.207l-.146-.147zm-1 10.5h-10v1h10zM2 13.5v-12H1v12zM2.5 1h8V0h-8zM13 3.5v10h1v-10zM10.146.854l3 3l.708-.708l-3-3zM2.5 14a.5.5 0 0 1-.5-.5H1A1.5 1.5 0 0 0 2.5 15zm10 1a1.5 1.5 0 0 0 1.5-1.5h-1a.5.5 0 0 1-.5.5zM2 1.5a.5.5 0 0 1 .5-.5V0A1.5 1.5 0 0 0 1 1.5z"
/>
</svg>
);
export const UserProfileIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 16 16"
>
<path
fill="currentColor"
d="M11 7c0 1.66-1.34 3-3 3S5 8.66 5 7s1.34-3 3-3s3 1.34 3 3"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M16 8c0 4.42-3.58 8-8 8s-8-3.58-8-8s3.58-8 8-8s8 3.58 8 8M4 13.75C4.16 13.484 5.71 11 7.99 11c2.27 0 3.83 2.49 3.99 2.75A6.98 6.98 0 0 0 14.99 8c0-3.87-3.13-7-7-7s-7 3.13-7 7c0 2.38 1.19 4.49 3.01 5.75"
clipRule="evenodd"
/>
</svg>
);
export const SettingsIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="m9.25 22l-.4-3.2q-.325-.125-.612-.3t-.563-.375L4.7 19.375l-2.75-4.75l2.575-1.95Q4.5 12.5 4.5 12.338v-.675q0-.163.025-.338L1.95 9.375l2.75-4.75l2.975 1.25q.275-.2.575-.375t.6-.3l.4-3.2h5.5l.4 3.2q.325.125.613.3t.562.375l2.975-1.25l2.75 4.75l-2.575 1.95q.025.175.025.338v.674q0 .163-.05.338l2.575 1.95l-2.75 4.75l-2.95-1.25q-.275.2-.575.375t-.6.3l-.4 3.2zm2.8-6.5q1.45 0 2.475-1.025T15.55 12t-1.025-2.475T12.05 8.5q-1.475 0-2.488 1.025T8.55 12t1.013 2.475T12.05 15.5"
/>
</svg>
);

View File

@ -0,0 +1,487 @@
import * as React from "react";
import { IconSvgProps } from "@/types/globals";
export const MenuBurgerIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2.5"
d="M3 6h18M3 12h18M3 18h18"
/>
</svg>
);
export const DashboardIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<path
fill="currentColor"
d="M13 9V3h8v6zM3 13V3h8v10zm10 8V11h8v10zM3 21v-6h8v6z"
/>
</svg>
);
export const HomeIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<g fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M2 12.204c0-2.289 0-3.433.52-4.381c.518-.949 1.467-1.537 3.364-2.715l2-1.241C9.889 2.622 10.892 2 12 2c1.108 0 2.11.622 4.116 1.867l2 1.241c1.897 1.178 2.846 1.766 3.365 2.715c.519.948.519 2.092.519 4.38v1.522c0 3.9 0 5.851-1.172 7.063C19.657 22 17.771 22 14 22h-4c-3.771 0-5.657 0-6.828-1.212C2 19.576 2 17.626 2 13.725z" />
<path strokeLinecap="round" d="M12 15v3" />
</g>
</svg>
);
export const Submenu1Icon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 48 48"
width={size || width}
{...props}
>
<defs>
<mask id="ipTData0">
<g
fill="none"
stroke="#fff"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="4"
>
<path d="M44 11v27c0 3.314-8.954 6-20 6S4 41.314 4 38V11" />
<path d="M44 29c0 3.314-8.954 6-20 6S4 32.314 4 29m40-9c0 3.314-8.954 6-20 6S4 23.314 4 20" />
<ellipse cx="24" cy="10" fill="#555" rx="20" ry="6" />
</g>
</mask>
</defs>
<path fill="currentColor" d="M0 0h48v48H0z" mask="url(#ipTData0)" />
</svg>
);
export const Submenu2Icon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 256 256"
width={size || width}
{...props}
>
<path
fill="currentColor"
d="M230.93 220a8 8 0 0 1-6.93 4H32a8 8 0 0 1-6.92-12c15.23-26.33 38.7-45.21 66.09-54.16a72 72 0 1 1 73.66 0c27.39 8.95 50.86 27.83 66.09 54.16a8 8 0 0 1 .01 8"
/>
</svg>
);
export const InfoCircleIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 1024 1024"
width={size || width}
{...props}
>
<path
fill="currentColor"
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448s448-200.6 448-448S759.4 64 512 64m0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372s372 166.6 372 372s-166.6 372-372 372"
/>
<path
fill="currentColor"
d="M464 336a48 48 0 1 0 96 0a48 48 0 1 0-96 0m72 112h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V456c0-4.4-3.6-8-8-8"
/>
</svg>
);
export const MinusCircleIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 16 16"
width={size || width}
{...props}
>
<g fill="currentColor">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16" />
<path d="M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8" />
</g>
</svg>
);
export const TableIcon = ({
size,
height = 24,
width = 22,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 24 22"
width={size || width}
strokeWidth="1.5"
stroke="currentColor"
{...props}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.375 19.5h17.25m-17.25 0a1.125 1.125 0 0 1-1.125-1.125M3.375 19.5h7.5c.621 0 1.125-.504 1.125-1.125m-9.75 0V5.625m0 12.75v-1.5c0-.621.504-1.125 1.125-1.125m18.375 2.625V5.625m0 12.75c0 .621-.504 1.125-1.125 1.125m1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125m0 3.75h-7.5A1.125 1.125 0 0 1 12 18.375m9.75-12.75c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125m19.5 0v1.5c0 .621-.504 1.125-1.125 1.125M2.25 5.625v1.5c0 .621.504 1.125 1.125 1.125m0 0h17.25m-17.25 0h7.5c.621 0 1.125.504 1.125 1.125M3.375 8.25c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125m17.25-3.75h-7.5c-.621 0-1.125.504-1.125 1.125m8.625-1.125c.621 0 1.125.504 1.125 1.125v1.5c0 .621-.504 1.125-1.125 1.125m-17.25 0h7.5m-7.5 0c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125M12 10.875v-1.5m0 1.5c0 .621-.504 1.125-1.125 1.125M12 10.875c0 .621.504 1.125 1.125 1.125m-2.25 0c.621 0 1.125.504 1.125 1.125M13.125 12h7.5m-7.5 0c-.621 0-1.125.504-1.125 1.125M20.625 12c.621 0 1.125.504 1.125 1.125v1.5c0 .621-.504 1.125-1.125 1.125m-17.25 0h7.5M12 14.625v-1.5m0 1.5c0 .621-.504 1.125-1.125 1.125M12 14.625c0 .621.504 1.125 1.125 1.125m-2.25 0c.621 0 1.125.504 1.125 1.125m0 1.5v-1.5m0 0c0-.621.504-1.125 1.125-1.125m0 0h7.5"
/>
</svg>
);
export const ArticleIcon = ({
size,
height = 20,
width = 20,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
height={size || height}
width={size || width}
viewBox="0 0 20 20"
{...props}
>
<path
fill="currentColor"
d="M5 1a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2zm0 3h5v1H5zm0 2h5v1H5zm0 2h5v1H5zm10 7H5v-1h10zm0-2H5v-1h10zm0-2H5v-1h10zm0-2h-4V4h4z"
/>
</svg>
);
export const MagazineIcon = ({
size,
height = 20,
width = 20,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
height={size || height}
width={size || width}
viewBox="0 0 128 128"
{...props}
>
<path
fill="#bdbdbd"
d="M-125.7 124.54V11.79c29.36 1.85 58.81 1.91 88.18.19c1.77-.1 3.21 1.08 3.21 2.66v107.04c0 1.58-1.44 2.94-3.21 3.05a727 727 0 0 1-88.18-.19"
/>
<path
fill="#e0e0e0"
d="M-125.7 124.54V11.79c27.11-5.31 54.34-8.57 81.45-9.76c1.64-.07 2.96 1.15 2.96 2.73V111.8c0 1.58-1.33 2.9-2.96 2.98c-27.11 1.19-54.34 4.45-81.45 9.76"
/>
<g fill="#757575">
<path d="M-92.84 42.86c-7.46 1-14.91 2.16-22.36 3.47v-3.22c7.45-1.31 14.9-2.47 22.36-3.47zm-12.76-15.72c-3.2.51-6.4 1.04-9.6 1.6v-8.47c3.2-.56 6.4-1.1 9.6-1.6zm12.17-1.78c-3.2.43-6.4.9-9.6 1.39v-8.47c3.2-.49 6.4-.95 9.6-1.39zm12.17-1.52q-4.8.54-9.6 1.17v-8.47q4.8-.63 9.6-1.17zm12.17-1.23c-3.2.29-6.4.61-9.6.95v-8.47c3.2-.35 6.4-.66 9.6-.95zm17.21 5.12a548 548 0 0 0-63.31 7.42v-.81c21.09-3.72 42.23-6.19 63.31-7.42zm-32.67 14.08c-2.21.26-4.41.54-6.62.83v-3.22c2.21-.29 4.41-.57 6.62-.83z" />
<path d="M-75.06 40.77c-3.6.37-7.21.77-10.81 1.2v-3.22c3.6-.44 7.21-.84 10.81-1.2zm12.29-1.11l-3.66.3v-3.22l3.66-.3z" />
<path d="M-65.38 39.87c-2.47.21-4.95.43-7.42.67v-3.22c2.47-.24 4.95-.46 7.42-.67zm10.32-.76c-2.02.13-4.05.27-6.07.42v-3.22c2.02-.15 4.05-.29 6.07-.42zm-49.89 14.57c-3.42.54-6.83 1.11-10.25 1.71v-3.22c3.41-.6 6.83-1.17 10.25-1.71zm9.51-1.4c-2.63.37-5.26.75-7.89 1.16v-3.22c2.63-.41 5.26-.79 7.89-1.16z" />
<path d="M-84.55 50.87c-3.95.47-7.89.98-11.84 1.54v-3.22c3.95-.56 7.89-1.07 11.84-1.54z" />
<path d="M-75.06 49.82c-3.6.37-7.21.77-10.81 1.2V47.8c3.6-.44 7.21-.84 10.81-1.2zm12.29-1.1l-3.66.3V45.8l3.66-.3z" />
<path d="M-61.13 48.59c-3.89.29-7.78.63-11.67 1.01v-3.22c3.89-.38 7.78-.71 11.67-1.01z" />
<path d="M-51.89 47.97c-3.08.18-6.16.39-9.25.62v-3.22c3.08-.23 6.17-.44 9.25-.62zm-49.5 14.22q-6.9 1.035-13.8 2.25v-3.22q6.9-1.215 13.8-2.25z" />
<path d="M-95.44 61.33c-2.63.37-5.26.75-7.89 1.16v-3.22c2.63-.41 5.26-.79 7.89-1.16zm10.89-1.4c-2.76.33-5.53.68-8.29 1.05v-3.22c2.76-.37 5.53-.72 8.29-1.05z" />
<path d="M-78.26 59.21c-2.54.27-5.07.56-7.61.87v-3.22c2.54-.31 5.07-.6 7.61-.87zm26.37 24.99q-12.075.705-24.18 1.95V55.76q12.09-1.245 24.18-1.95zm-38.55-14.48c-8.25 1.07-16.51 2.34-24.75 3.79v-3.22c8.24-1.45 16.5-2.72 24.75-3.79z" />
<path d="M-95.44 70.39c-1.31.18-2.63.37-3.94.56v-3.22c1.31-.19 2.63-.38 3.94-.56zm10.89-1.41c-2.21.26-4.41.54-6.62.83v-3.22c2.21-.29 4.41-.57 6.62-.83z" />
<path d="M-78.32 68.28c-2.51.27-5.03.56-7.54.86v-3.22c2.51-.31 5.03-.59 7.54-.86zm-23.07 12.03q-6.9 1.035-13.8 2.25v-3.22q6.9-1.215 13.8-2.25z" />
<path d="M-98.16 79.83c-1.72.25-3.44.51-5.17.77v-3.22c1.72-.27 3.44-.52 5.17-.77zm13.61-1.79q-5.445.645-10.89 1.41v-3.22c3.63-.51 7.26-.97 10.89-1.41z" />
<path d="M-80.46 77.57c-1.8.2-3.6.41-5.41.63v-3.22c1.8-.22 3.6-.43 5.41-.63zm-16.95 11.21c-5.93.85-11.86 1.79-17.79 2.84V88.4c5.92-1.04 11.85-1.99 17.79-2.84z" />
<path d="M-92.54 88.1c-2.28.31-4.56.62-6.84.96v-3.22q3.42-.495 6.84-.96zm7.99-1.01c-1.75.21-3.5.43-5.25.65v-3.22c1.75-.23 3.5-.44 5.25-.65z" />
<path d="M-78.32 86.39c-2.51.27-5.03.56-7.54.86v-3.22c2.51-.31 5.03-.59 7.54-.86zm-23.07 12.03q-6.9 1.035-13.8 2.25v-3.22q6.9-1.215 13.8-2.25zm14.22-1.95q-6.105.75-12.21 1.65V94.9q6.105-.9 12.21-1.65z" />
<path d="M-84.55 96.15c-2.21.26-4.41.54-6.62.83v-3.22c2.21-.29 4.41-.57 6.62-.83z" />
<path d="M-75.06 95.1c-3.6.37-7.21.77-10.81 1.2v-3.22c3.6-.44 7.21-.84 10.81-1.2zm12.29-1.1l-3.66.3v-3.22l3.66-.3zm-5.64.47c-1.46.13-2.93.27-4.39.41v-3.22c1.46-.14 2.93-.28 4.39-.41z" />
<path d="M-51.89 93.25c-6 .35-12.01.8-18.01 1.35v-3.22c6.01-.55 12.01-1 18.01-1.35zm-43.56 13.36c-6.59.92-13.17 1.96-19.75 3.11v-3.22c6.58-1.16 13.16-2.2 19.75-3.11zm22.09-2.62c-3.48.34-6.96.72-10.44 1.13v-3.22c3.48-.41 6.96-.78 10.44-1.13zm-13.53 1.5c-2.18.27-4.36.55-6.55.85v-3.22c2.18-.3 4.36-.58 6.55-.85z" />
</g>
<path
fill="#eee"
d="M15.71 280.41V170.86h76.08a2.77 2.77 0 0 1 2.77 2.77v104.01a2.77 2.77 0 0 1-2.77 2.77z"
/>
<g fill="#757575">
<path d="M25.53 203.19h20.88v3.13H25.53zm0-22.19h8.96v8.23h-8.96zm11.36 0h8.96v8.23h-8.96zm11.36 0h8.96v8.23h-8.96zm11.36 0h8.96v8.23h-8.96zm-34.08 13.66h59.12v.79H25.53zm22.44 8.53h6.18v3.13h-6.18z" />
<path d="M52.92 203.19h10.09v3.13H52.92zm18.14 0h3.42v3.13h-3.42z" />
<path d="M65.11 203.19h6.93v3.13h-6.93zm10.9 0h5.67v3.13h-5.67zm-50.48 8.8h9.57v3.13h-9.57zm11.08 0h7.37v3.13h-7.37z" />
<path d="M43.1 211.99h11.05v3.13H43.1z" />
<path d="M52.92 211.99h10.09v3.13H52.92zm18.14 0h3.42v3.13h-3.42z" />
<path d="M65.11 211.99H76v3.13H65.11zm10.9 0h8.64v3.13h-8.64zm-50.48 8.8h12.89v3.13H25.53z" />
<path d="M36.61 220.79h7.37v3.13h-7.37zm9.8 0h7.74v3.13h-7.74z" />
<path d="M52.92 220.79h7.1v3.13h-7.1zm9.15 0h22.58v29.53H62.07zm-36.54 8.8h23.11v3.13H25.53z" />
<path d="M40.3 229.59h3.68v3.13H40.3zm7.67 0h6.18v3.13h-6.18z" />
<path d="M52.92 229.59h7.04v3.13h-7.04zm-27.39 8.8h12.89v3.13H25.53z" />
<path d="M36.61 238.39h4.82v3.13h-4.82zm7.37 0h10.17v3.13H43.98z" />
<path d="M52.92 238.39h5.04v3.13h-5.04zm-27.39 8.79h16.61v3.13H25.53z" />
<path d="M40.3 247.18h6.38v3.13H40.3zm8.95 0h4.9v3.13h-4.9z" />
<path d="M52.92 247.18h7.04v3.13h-7.04zm-27.39 8.8h12.89v3.13H25.53zm14.77 0h11.39v3.13H40.3z" />
<path d="M47.97 255.98h6.18v3.13h-6.18z" />
<path d="M52.92 255.98h10.09v3.13H52.92zm18.14 0h3.42v3.13h-3.42zm-5.95 0h4.1v3.13h-4.1z" />
<path d="M67.82 255.98h16.82v3.13H67.82zm-42.29 8.8h18.44v3.13H25.53zm29.32 0h9.74v3.13h-9.74zm-9 0h6.11v3.13h-6.11z" />
</g>
<path
fill="#bdbdbd"
d="M16.62 124.27V14.04c30.52 2.2 61.18 2.27 91.71.21c1.68-.11 3.05 1.04 3.05 2.58v104.65c0 1.54-1.36 2.89-3.05 3a659 659 0 0 1-91.71-.21"
/>
<path
fill="#e0e0e0"
d="M16.62 124.25V14.02C44.36 7.91 72.21 3.9 99.95 2.03c1.53-.1 2.77 1.07 2.77 2.61v104.65c0 1.54-1.24 2.87-2.77 2.97c-27.74 1.87-55.59 5.88-83.33 11.99"
/>
<path
fill="none"
stroke="#616161"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="5"
d="M28.75 49.34c20.6-4.08 41.25-7 61.84-8.74M28.75 63.23a565 565 0 0 1 26.45-4.6M28.75 77.11a565 565 0 0 1 26.45-4.6m-26.45 33.06c20.6-4.08 41.25-7 61.84-8.74M28.75 91a565 565 0 0 1 26.45-4.6"
/>
<path
fill="#616161"
d="M64.86 87.55a560 560 0 0 1 24.67-2.69c1.49-.13 2.69-1.44 2.69-2.94V54.54c0-1.5-1.21-2.61-2.69-2.48c-8.22.71-16.44 1.61-24.67 2.69c-1.49.2-2.7 1.58-2.7 3.07V85.2c.01 1.5 1.21 2.55 2.7 2.35m-34.4-52.14c2.03-.4 4.05-.78 6.08-1.15c1.49-.27 2.69-1.7 2.69-3.2v-7.02c0-1.5-1.21-2.49-2.69-2.22c-2.03.37-4.05.76-6.08 1.15c-1.49.29-2.69 1.75-2.69 3.24v7.02c-.01 1.5 1.2 2.47 2.69 2.18m15.96-2.88c2.03-.34 4.05-.66 6.08-.97c1.49-.23 2.7-1.62 2.7-3.12v-7.02c0-1.5-1.21-2.53-2.7-2.3c-2.03.31-4.06.64-6.08.97c-1.49.25-2.69 1.67-2.69 3.16v7.02c0 1.5 1.2 2.51 2.69 2.26m15.97-2.41c2.03-.28 4.06-.54 6.08-.8c1.49-.19 2.7-1.54 2.7-3.04v-7.02c0-1.5-1.21-2.57-2.7-2.38c-2.03.25-4.06.52-6.08.8c-1.49.2-2.7 1.59-2.7 3.08v7.02c.01 1.5 1.22 2.54 2.7 2.34"
/>
<path
fill="#e0e0e0"
d="M374.07 165.73V44.63h92.1a3.06 3.06 0 0 1 3.06 3.06v114.98a3.06 3.06 0 0 1-3.06 3.06z"
/>
<path
fill="none"
stroke="#616161"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="5"
d="M387.48 86.21h68.34m-68.34 15.26h29.23m-29.23 15.26h29.23M387.48 148h68.34m-68.34-16.01h29.23"
/>
<path
fill="#616161"
d="M427.38 134.75h27.26c1.64 0 2.98-1.33 2.98-2.98v-30.08c0-1.64-1.33-2.98-2.98-2.98h-27.26c-1.64 0-2.98 1.33-2.98 2.98v30.08a2.987 2.987 0 0 0 2.98 2.98m-38.01-63.47h6.72c1.64 0 2.98-1.33 2.98-2.98v-7.71c0-1.64-1.33-2.98-2.98-2.98h-6.72c-1.64 0-2.98 1.33-2.98 2.98v7.71c0 1.65 1.33 2.98 2.98 2.98m17.64 0h6.72c1.64 0 2.98-1.33 2.98-2.98v-7.71c0-1.64-1.33-2.98-2.98-2.98h-6.72c-1.64 0-2.98 1.33-2.98 2.98v7.71a2.987 2.987 0 0 0 2.98 2.98m17.65 0h6.72c1.64 0 2.98-1.33 2.98-2.98v-7.71c0-1.64-1.33-2.98-2.98-2.98h-6.72c-1.64 0-2.98 1.33-2.98 2.98v7.71c0 1.65 1.33 2.98 2.98 2.98"
/>
<path
fill="#bdbdbd"
d="M479.86 165.73V44.63h92.1a3.06 3.06 0 0 1 3.06 3.06v114.98a3.06 3.06 0 0 1-3.06 3.06z"
/>
</svg>
);
export const StaticPageIcon = ({
size,
height = 20,
width = 20,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
viewBox="0 0 2048 2048"
{...props}
>
<path
fill="currentColor"
d="M1755 512h-475V37zm37 128v1408H128V0h1024v640z"
/>
</svg>
);
export const MasterUsersIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
viewBox="0 0 640 512"
{...props}
>
<path
fill="currentColor"
d="M144 0a80 80 0 1 1 0 160a80 80 0 1 1 0-160m368 0a80 80 0 1 1 0 160a80 80 0 1 1 0-160M0 298.7C0 239.8 47.8 192 106.7 192h42.7c15.9 0 31 3.5 44.6 9.7c-1.3 7.2-1.9 14.7-1.9 22.3c0 38.2 16.8 72.5 43.3 96H21.3C9.6 320 0 310.4 0 298.7M405.3 320h-.7c26.6-23.5 43.3-57.8 43.3-96c0-7.6-.7-15-1.9-22.3c13.6-6.3 28.7-9.7 44.6-9.7h42.7c58.9 0 106.7 47.8 106.7 106.7c0 11.8-9.6 21.3-21.3 21.3H405.4zM224 224a96 96 0 1 1 192 0a96 96 0 1 1-192 0m-96 261.3c0-73.6 59.7-133.3 133.3-133.3h117.3c73.7 0 133.4 59.7 133.4 133.3c0 14.7-11.9 26.7-26.7 26.7H154.6c-14.7 0-26.7-11.9-26.7-26.7z"
/>
</svg>
);
export const MasterRoleIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="M15 21h-2a2 2 0 0 1 0-4h2v-2h-2a4 4 0 0 0 0 8h2Zm8-2a4 4 0 0 1-4 4h-2v-2h2a2 2 0 0 0 0-4h-2v-2h2a4 4 0 0 1 4 4"
/>
<path
fill="currentColor"
d="M14 18h4v2h-4zm-7 1a6 6 0 0 1 .09-1H3v-1.4c0-2 4-3.1 6-3.1a8.6 8.6 0 0 1 1.35.125A5.95 5.95 0 0 1 13 13h5V4a2.006 2.006 0 0 0-2-2h-4.18a2.988 2.988 0 0 0-5.64 0H2a2.006 2.006 0 0 0-2 2v14a2.006 2.006 0 0 0 2 2h5.09A6 6 0 0 1 7 19M9 2a1 1 0 1 1-1 1a1.003 1.003 0 0 1 1-1m0 4a3 3 0 1 1-3 3a2.996 2.996 0 0 1 3-3"
/>
</svg>
);
export const MasterUserLevelIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
viewBox="0 0 640 512"
{...props}
>
<path
fill="currentColor"
d="M192 256c61.9 0 112-50.1 112-112S253.9 32 192 32S80 82.1 80 144s50.1 112 112 112m76.8 32h-8.3c-20.8 10-43.9 16-68.5 16s-47.6-6-68.5-16h-8.3C51.6 288 0 339.6 0 403.2V432c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48v-28.8c0-63.6-51.6-115.2-115.2-115.2M480 256c53 0 96-43 96-96s-43-96-96-96s-96 43-96 96s43 96 96 96m48 32h-3.8c-13.9 4.8-28.6 8-44.2 8s-30.3-3.2-44.2-8H432c-20.4 0-39.2 5.9-55.7 15.4c24.4 26.3 39.7 61.2 39.7 99.8v38.4c0 2.2-.5 4.3-.6 6.4H592c26.5 0 48-21.5 48-48c0-61.9-50.1-112-112-112"
/>
</svg>
);
export const MasterCategoryIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
viewBox="0 0 32 32"
{...props}
>
<path
fill="currentColor"
d="M14 25h14v2H14zm-6.83 1l-2.58 2.58L6 30l4-4l-4-4l-1.42 1.41zM14 15h14v2H14zm-6.83 1l-2.58 2.58L6 20l4-4l-4-4l-1.42 1.41zM14 5h14v2H14zM7.17 6L4.59 8.58L6 10l4-4l-4-4l-1.42 1.41z"
/>
</svg>
);
export const AddvertiseIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 8q-1.65 0-2.825 1.175T8 12q0 1.125.563 2.075t1.562 1.475q.4.2.563.587t-.013.788q-.175.35-.525.525t-.7 0q-1.575-.75-2.512-2.225T6 12q0-2.5 1.75-4.25T12 6q1.775 0 3.263.938T17.475 9.5q.15.35-.012.7t-.513.5q-.4.175-.8 0t-.6-.575q-.525-1-1.475-1.562T12 8m0-4Q8.65 4 6.325 6.325T4 12q0 3.15 2.075 5.4t5.2 2.55q.425.05.737.375t.288.75t-.313.7t-.712.25q-1.95-.125-3.638-.975t-2.95-2.213t-1.975-3.125T2 12q0-2.075.788-3.9t2.137-3.175T8.1 2.788T12 2q3.925 0 6.838 2.675t3.187 6.6q.05.4-.237.688t-.713.312t-.762-.275t-.388-.725q-.375-3-2.612-5.137T12 4m7.55 17.5l-3.3-3.275l-.75 2.275q-.125.35-.475.338t-.475-.363L12.275 12.9q-.1-.275.125-.5t.5-.125l7.575 2.275q.35.125.363.475t-.338.475l-2.275.75l3.3 3.3q.425.425.425.975t-.425.975t-.987.425t-.988-.425"
/>
</svg>
);
export const SuggestionsIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M17.175 14H7.5q-1.875 0-3.187-1.312T3 9.5t1.313-3.187T7.5 5q.425 0 .713.288T8.5 6t-.288.713T7.5 7q-1.05 0-1.775.725T5 9.5t.725 1.775T7.5 12h9.675L14.3 9.1q-.275-.275-.288-.687T14.3 7.7q.275-.275.7-.275t.7.275l4.6 4.6q.3.3.3.7t-.3.7l-4.6 4.6q-.3.3-.7.288t-.7-.313q-.275-.3-.288-.7t.288-.7z"
/>
</svg>
);
export const CommentIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M6.5 13.5h11v-1h-11zm0-3h11v-1h-11zm0-3h11v-1h-11zM4.616 17q-.691 0-1.153-.462T3 15.385V4.615q0-.69.463-1.153T4.615 3h14.77q.69 0 1.152.462T21 4.615v15.462L17.923 17z"
/>
</svg>
);

View File

@ -0,0 +1,36 @@
"use client";
export default function Category() {
const categories = [
"PON XXI",
"OPERASI KETUPAT 2025",
"HUT HUMAS KE-74",
"OPERASI ZEBRA 2025",
"OPERASI MANTAP PRAJA & PILKADA 2024",
"PERS RILIS",
"LIPUTAN KEGIATAN",
"UNGKAP KASUS",
"INFOGRAFIS",
"SEPUTAR PRESTASI",
];
return (
<section className="px-4 py-10">
<div className="max-w-[1350px] mx-auto bg-white rounded-xl shadow-md p-6">
<h2 className="text-xl font-semibold mb-5">
10 Kategori Paling Populer
</h2>
<div className="flex flex-wrap gap-3">
{categories.map((category, index) => (
<button
key={index}
className="px-4 py-2 rounded border border-gray-300 text-gray-700 text-sm font-medium hover:bg-gray-100 transition"
>
{category}
</button>
))}
</div>
</div>
</section>
);
}

Some files were not shown because too many files have changed in this diff Show More