fix: all create, edit dan detail content
This commit is contained in:
parent
2b545ec51a
commit
82062bc3ad
|
|
@ -12,7 +12,7 @@ const AudioTabs = () => {
|
|||
<div className="w-full">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<TabsList className="inline-flex h-10 bg-gray-50 border border-gray-200 p-1 rounded-lg shadow-sm">
|
||||
{/* <TabsList className="inline-flex h-10 bg-gray-50 border border-gray-200 p-1 rounded-lg shadow-sm">
|
||||
<TabsTrigger
|
||||
value="submitted"
|
||||
className="text-sm font-medium px-6 py-2 h-8 rounded-md transition-all duration-200 data-[state=active]:bg-white data-[state=active]:text-blue-600 data-[state=active]:shadow-sm data-[state=inactive]:text-gray-600 data-[state=inactive]:hover:text-gray-800"
|
||||
|
|
@ -31,7 +31,7 @@ const AudioTabs = () => {
|
|||
Waiting Approval
|
||||
</div>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</TabsList> */}
|
||||
</div>
|
||||
|
||||
<TabsContent value="submitted" className="mt-0">
|
||||
|
|
@ -40,11 +40,11 @@ const AudioTabs = () => {
|
|||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="pending" className="mt-0">
|
||||
{/* <TabsContent value="pending" className="mt-0">
|
||||
<div className="bg-white rounded-lg border border-gray-200 shadow-sm">
|
||||
<PendingApprovalTable typeId={4} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</TabsContent> */}
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const DocumentTabs = () => {
|
|||
<div className="w-full">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<TabsList className="inline-flex h-10 bg-gray-50 border border-gray-200 p-1 rounded-lg shadow-sm">
|
||||
{/* <TabsList className="inline-flex h-10 bg-gray-50 border border-gray-200 p-1 rounded-lg shadow-sm">
|
||||
<TabsTrigger
|
||||
value="submitted"
|
||||
className="text-sm font-medium px-6 py-2 h-8 rounded-md transition-all duration-200 data-[state=active]:bg-white data-[state=active]:text-blue-600 data-[state=active]:shadow-sm data-[state=inactive]:text-gray-600 data-[state=inactive]:hover:text-gray-800"
|
||||
|
|
@ -31,7 +31,7 @@ const DocumentTabs = () => {
|
|||
Waiting Approval
|
||||
</div>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</TabsList> */}
|
||||
</div>
|
||||
|
||||
<TabsContent value="submitted" className="mt-0">
|
||||
|
|
@ -40,11 +40,11 @@ const DocumentTabs = () => {
|
|||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="pending" className="mt-0">
|
||||
{/* <TabsContent value="pending" className="mt-0">
|
||||
<div className="bg-white rounded-lg border border-gray-200 shadow-sm">
|
||||
<PendingApprovalTable typeId={3} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</TabsContent> */}
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export default function ManagementUser() {
|
|||
return (
|
||||
<div>
|
||||
<SiteBreadcrumb />
|
||||
<section className="flex flex-col gap-2 bg-white dark:bg-black rounded-lg p-3 mt-5 border">
|
||||
<section className="flex flex-col gap-2 bg-white rounded-lg p-3 mt-5 border">
|
||||
<div className="flex justify-between py-3">
|
||||
<p className="text-lg">
|
||||
Data User
|
||||
|
|
|
|||
|
|
@ -750,7 +750,7 @@ function TenantSettingsContentTable() {
|
|||
onOpenChange={setIsHierarchyExpanded}
|
||||
>
|
||||
<CollapsibleTrigger asChild>
|
||||
<CardHeader className="cursor-pointer hover:bg-gray-50 dark:hover:bg-black transition-colors">
|
||||
<CardHeader className="cursor-pointer hover:bg-gray-50 transition-colors">
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<UsersIcon className="h-5 w-5" />
|
||||
|
|
|
|||
|
|
@ -359,11 +359,12 @@ export default function FormVideoDetail() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-3 py-3">
|
||||
<div className="px-3 py-3 gap-2">
|
||||
<Label>Publish Target</Label>
|
||||
{[5, 6].map((target) => (
|
||||
<div key={target} className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
<input
|
||||
type="checkbox"
|
||||
id={String(target)}
|
||||
checked={selectedPublishers.includes(target)}
|
||||
onChange={() => handleCheckboxChange(target)}
|
||||
|
|
|
|||
|
|
@ -379,7 +379,7 @@ type Option = {
|
|||
label: string;
|
||||
};
|
||||
|
||||
export default function FormVideo() {
|
||||
export default function FormVideo() {
|
||||
const MySwal = withReactContent(Swal);
|
||||
const router = useRouter();
|
||||
const editor = useRef(null);
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ import { TimeIcon, TimesIcon } from "@/components/icons";
|
|||
|
||||
const CustomEditor = dynamic(
|
||||
() => import("@/components/editor/custom-editor"),
|
||||
{ ssr: false }
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
const videoSchema = z.object({
|
||||
|
|
@ -59,12 +59,12 @@ const videoSchema = z.object({
|
|||
files.every(
|
||||
(file: File) =>
|
||||
["video/mp4", "video/mov", "video/avi"].includes(file.type) &&
|
||||
file.size <= 100 * 1024 * 1024
|
||||
file.size <= 100 * 1024 * 1024,
|
||||
),
|
||||
{
|
||||
message:
|
||||
"Hanya file .mp4, .mov, .avi, maksimal 100MB yang diperbolehkan.",
|
||||
}
|
||||
},
|
||||
),
|
||||
publishedFor: z
|
||||
.array(z.string())
|
||||
|
|
@ -129,7 +129,7 @@ export default function FormVideoUpdate() {
|
|||
}
|
||||
|
||||
const filesWithPreview = acceptedFiles.map((file) =>
|
||||
Object.assign(file, { preview: URL.createObjectURL(file) })
|
||||
Object.assign(file, { preview: URL.createObjectURL(file) }),
|
||||
);
|
||||
|
||||
setFiles((prev) => {
|
||||
|
|
@ -201,7 +201,7 @@ export default function FormVideoUpdate() {
|
|||
|
||||
const handleCheckboxChange = (value: string) => {
|
||||
setPublishedFor((prev) =>
|
||||
prev.includes(value) ? prev.filter((v) => v !== value) : [...prev, value]
|
||||
prev.includes(value) ? prev.filter((v) => v !== value) : [...prev, value],
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -210,20 +210,31 @@ export default function FormVideoUpdate() {
|
|||
loading();
|
||||
|
||||
const payload = {
|
||||
aiArticleId: detail?.aiArticleId ?? "",
|
||||
categoryIds: selectedCategory ?? "",
|
||||
createdById: detail?.createdById ?? "",
|
||||
aiArticleId: detail.aiArticleId,
|
||||
categoryIds: selectedCategory,
|
||||
description: htmlToString(data.description),
|
||||
htmlDescription: data.description,
|
||||
isDraft: false,
|
||||
isPublish: true,
|
||||
slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"),
|
||||
statusId: detail?.statusId ?? 1,
|
||||
tags: tags.join(","),
|
||||
title: data.title,
|
||||
typeId: detail?.typeId ?? 2,
|
||||
typeId: detail.typeId,
|
||||
slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"),
|
||||
};
|
||||
|
||||
// const payload = {
|
||||
// aiArticleId: detail?.aiArticleId ?? "",
|
||||
// categoryIds: selectedCategory ?? "",
|
||||
// createdById: detail?.createdById ?? "",
|
||||
// description: htmlToString(data.description),
|
||||
// htmlDescription: data.description,
|
||||
// isDraft: false,
|
||||
// isPublish: true,
|
||||
// slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"),
|
||||
// statusId: detail?.statusId ?? 1,
|
||||
// tags: tags.join(","),
|
||||
// title: data.title,
|
||||
// typeId: detail?.typeId ?? 2,
|
||||
// };
|
||||
|
||||
console.log("📤 Payload Update:", payload);
|
||||
|
||||
const res = await updateArticle(Number(id), payload);
|
||||
|
|
@ -399,7 +410,7 @@ export default function FormVideoUpdate() {
|
|||
<Controller
|
||||
control={control}
|
||||
name="creatorName"
|
||||
render={({ field }) => <Input {...field} />}
|
||||
render={({ field }) => <Input {...field} />}
|
||||
/>
|
||||
|
||||
<div className="mt-3 space-y-2">
|
||||
|
|
@ -470,23 +481,19 @@ export default function FormVideoUpdate() {
|
|||
? isAllChecked
|
||||
: field.value.includes(option.id);
|
||||
|
||||
const handleChange = () => {
|
||||
const handleChange = (checked: boolean) => {
|
||||
let updated: string[] = [];
|
||||
|
||||
if (option.id === "all") {
|
||||
updated = isAllChecked
|
||||
? []
|
||||
: options
|
||||
updated = checked
|
||||
? options
|
||||
.filter((opt: any) => opt.id !== "all")
|
||||
.map((opt: any) => opt.id);
|
||||
.map((opt: any) => opt.id)
|
||||
: [];
|
||||
} else {
|
||||
updated = isChecked
|
||||
? field.value.filter((val) => val !== option.id)
|
||||
: [...field.value, option.id];
|
||||
|
||||
if (isAllChecked && option.id !== "all") {
|
||||
updated = updated.filter((val) => val !== "all");
|
||||
}
|
||||
updated = checked
|
||||
? [...field.value, option.id]
|
||||
: field.value.filter((val) => val !== option.id);
|
||||
}
|
||||
|
||||
field.onChange(updated);
|
||||
|
|
@ -498,12 +505,20 @@ export default function FormVideoUpdate() {
|
|||
key={option.id}
|
||||
className="flex gap-2 items-center"
|
||||
>
|
||||
<Checkbox
|
||||
<input
|
||||
type="checkbox"
|
||||
id={option.id}
|
||||
checked={isChecked}
|
||||
onChange={(e) => handleChange(e.target.checked)}
|
||||
className="border"
|
||||
/>
|
||||
|
||||
{/* <Checkbox
|
||||
id={option.id}
|
||||
checked={isChecked}
|
||||
onCheckedChange={handleChange}
|
||||
className="border"
|
||||
/>
|
||||
/> */}
|
||||
<Label htmlFor={option.id}>{option.label}</Label>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -521,14 +536,19 @@ export default function FormVideoUpdate() {
|
|||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 mt-5">
|
||||
<Button type="submit" className="border rounded-lg">Update</Button>
|
||||
<Button
|
||||
<button
|
||||
type="submit"
|
||||
className="border border-black hover:bg-black hover:text-white rounded-lg px-8 py-4"
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="border border-black hover:bg-black hover:text-white rounded-lg px-8 py-4"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -84,9 +84,10 @@ type Category = {
|
|||
|
||||
type FileType = {
|
||||
id: number;
|
||||
secondaryUrl: string;
|
||||
thumbnailFileUrl: string;
|
||||
fileName: string;
|
||||
secondaryUrl?: string | null;
|
||||
fileUrl?: string | null;
|
||||
preview?: string;
|
||||
};
|
||||
|
||||
type Detail = {
|
||||
|
|
@ -526,6 +527,47 @@ export default function FormAudioDetail() {
|
|||
|
||||
<div className="w-full">
|
||||
<Label className="text-xl space-y-2">File Media</Label>
|
||||
|
||||
<div className="space-y-4 mt-4">
|
||||
{files.length === 0 ? (
|
||||
<p className="text-center text-gray-500">
|
||||
Tidak ada file media
|
||||
</p>
|
||||
) : (
|
||||
files.map((file, idx) => {
|
||||
const audioSrc = file.secondaryUrl || file.fileUrl;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex flex-col gap-2 border p-2 rounded-md"
|
||||
>
|
||||
<p className="text-sm font-medium truncate">
|
||||
{file.fileName}
|
||||
</p>
|
||||
|
||||
{audioSrc ? (
|
||||
<audio
|
||||
controls
|
||||
src={audioSrc}
|
||||
className="w-full rounded"
|
||||
>
|
||||
Browser tidak mendukung audio.
|
||||
</audio>
|
||||
) : (
|
||||
<p className="text-xs text-red-500">
|
||||
Audio source tidak tersedia
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</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">
|
||||
|
|
@ -541,7 +583,7 @@ export default function FormAudioDetail() {
|
|||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ const CustomEditor = dynamic(
|
|||
() => {
|
||||
return import("@/components/editor/custom-editor");
|
||||
},
|
||||
{ ssr: false }
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
export default function FormAudio() {
|
||||
|
|
@ -118,7 +118,7 @@ export default function FormAudio() {
|
|||
const [isGeneratedArticle, setIsGeneratedArticle] = useState(false);
|
||||
const [articleBody, setArticleBody] = useState<string>("");
|
||||
const [selectedArticleId, setSelectedArticleId] = useState<string | null>(
|
||||
null
|
||||
null,
|
||||
);
|
||||
const [selectedMainKeyword, setSelectedMainKeyword] = useState("");
|
||||
const [selectedSize, setSelectedSize] = useState("");
|
||||
|
|
@ -149,7 +149,7 @@ export default function FormAudio() {
|
|||
type FileWithPreview = File & { preview: string };
|
||||
const userId = Cookies.get("userId");
|
||||
|
||||
const options: Option[] = [
|
||||
const options: Option[] = [
|
||||
{ id: "all", label: "SEMUA" },
|
||||
{ id: "4", label: "UMUM" },
|
||||
{ id: "5", label: "JOURNALIS" },
|
||||
|
|
@ -181,7 +181,7 @@ export default function FormAudio() {
|
|||
}
|
||||
|
||||
const filesWithPreview = acceptedFiles.map((file) =>
|
||||
Object.assign(file, { preview: URL.createObjectURL(file) })
|
||||
Object.assign(file, { preview: URL.createObjectURL(file) }),
|
||||
);
|
||||
|
||||
setFiles((prevFiles) => [...prevFiles, ...filesWithPreview]);
|
||||
|
|
@ -216,11 +216,11 @@ export default function FormAudio() {
|
|||
files.every(
|
||||
(file: File) =>
|
||||
["audio/mpeg", "audio/wav", "audio/mp3"].includes(file.type) &&
|
||||
file.size <= 100 * 1024 * 1024
|
||||
file.size <= 100 * 1024 * 1024,
|
||||
),
|
||||
{
|
||||
message: "Hanya file .mp3, .wav, maksimal 100MB yang diperbolehkan.",
|
||||
}
|
||||
},
|
||||
),
|
||||
categoryId: z.string().min(1, { message: "Kategori wajib dipilih." }),
|
||||
tags: z
|
||||
|
|
@ -430,7 +430,7 @@ export default function FormAudio() {
|
|||
const articleData = await waitForStatusUpdate();
|
||||
const cleanArticleBody = articleData?.articleBody?.replace(
|
||||
/<img[^>]*>/g,
|
||||
""
|
||||
"",
|
||||
);
|
||||
const articleImagesData = articleData?.imagesUrl?.split(",");
|
||||
setArticleBody(cleanArticleBody || "");
|
||||
|
|
@ -504,7 +504,7 @@ export default function FormAudio() {
|
|||
|
||||
if (scheduleId && scheduleType === "3") {
|
||||
const findCategory = resCategory.find((o) =>
|
||||
o.name.toLowerCase().includes("pers rilis")
|
||||
o.name.toLowerCase().includes("pers rilis"),
|
||||
);
|
||||
|
||||
if (findCategory) {
|
||||
|
|
@ -537,7 +537,7 @@ export default function FormAudio() {
|
|||
setPublishedFor(
|
||||
options
|
||||
.filter((opt: any) => opt.id !== "all")
|
||||
.map((opt: any) => opt.id)
|
||||
.map((opt: any) => opt.id),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -569,8 +569,8 @@ export default function FormAudio() {
|
|||
const finalDescription = isSwitchOn
|
||||
? data.description
|
||||
: selectedFileType === "rewrite"
|
||||
? data.rewriteDescription
|
||||
: data.descriptionOri;
|
||||
? data.rewriteDescription
|
||||
: data.descriptionOri;
|
||||
if (!finalDescription?.trim()) {
|
||||
MySwal.fire("Error", "Deskripsi tidak boleh kosong.", "error");
|
||||
return;
|
||||
|
|
@ -661,7 +661,7 @@ export default function FormAudio() {
|
|||
MySwal.fire(
|
||||
"Error",
|
||||
response.message || "Failed to create article",
|
||||
"error"
|
||||
"error",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -689,7 +689,7 @@ export default function FormAudio() {
|
|||
MySwal.fire(
|
||||
"Error",
|
||||
uploadResponse.message || "Failed to upload files",
|
||||
"error"
|
||||
"error",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -706,19 +706,19 @@ export default function FormAudio() {
|
|||
try {
|
||||
const thumbnailResponse = await uploadArticleThumbnail(
|
||||
articleId,
|
||||
thumbnailFormData
|
||||
thumbnailFormData,
|
||||
);
|
||||
|
||||
if (thumbnailResponse?.error) {
|
||||
console.warn(
|
||||
"Thumbnail upload failed:",
|
||||
thumbnailResponse.message
|
||||
thumbnailResponse.message,
|
||||
);
|
||||
// Don't fail the whole process if thumbnail upload fails
|
||||
} else {
|
||||
console.log(
|
||||
"Thumbnail uploaded successfully:",
|
||||
thumbnailResponse
|
||||
thumbnailResponse,
|
||||
);
|
||||
}
|
||||
} catch (thumbnailError) {
|
||||
|
|
@ -731,7 +731,7 @@ export default function FormAudio() {
|
|||
MySwal.fire(
|
||||
"Error",
|
||||
"Failed to upload files. Please try again.",
|
||||
"error"
|
||||
"error",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -774,7 +774,7 @@ export default function FormAudio() {
|
|||
idx: number,
|
||||
id: string,
|
||||
file: any,
|
||||
duration: string
|
||||
duration: string,
|
||||
) {
|
||||
console.log(idx, id, file, duration);
|
||||
|
||||
|
|
@ -810,7 +810,7 @@ export default function FormAudio() {
|
|||
onChunkComplete: (
|
||||
chunkSize: any,
|
||||
bytesAccepted: any,
|
||||
bytesTotal: any
|
||||
bytesTotal: any,
|
||||
) => {
|
||||
const uploadPersen = Math.floor((bytesAccepted / bytesTotal) * 100);
|
||||
progressInfo[idx].percentage = uploadPersen;
|
||||
|
|
@ -1053,6 +1053,36 @@ export default function FormAudio() {
|
|||
<div className="flex flex-row items-center gap-3 py-2">
|
||||
<Label>Ai Assistance</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSwitchOn}
|
||||
onChange={(e) => setIsSwitchOn(e.target.checked)}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div
|
||||
className="
|
||||
w-11 h-6
|
||||
bg-gray-300
|
||||
rounded-full
|
||||
peer
|
||||
peer-checked:bg-blue-600
|
||||
transition-colors
|
||||
after:content-['']
|
||||
after:absolute
|
||||
after:top-[2px]
|
||||
after:left-[2px]
|
||||
after:bg-white
|
||||
after:rounded-full
|
||||
after:h-5
|
||||
after:w-5
|
||||
after:transition-transform
|
||||
peer-checked:after:translate-x-5
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{/* <div className="flex items-center gap-3">
|
||||
<Switch
|
||||
defaultChecked={isSwitchOn}
|
||||
color="primary"
|
||||
|
|
@ -1061,7 +1091,7 @@ export default function FormAudio() {
|
|||
setIsSwitchOn(checked)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
{isSwitchOn && (
|
||||
<div>
|
||||
|
|
@ -1564,19 +1594,19 @@ export default function FormAudio() {
|
|||
? isAllChecked
|
||||
: field.value.includes(option.id);
|
||||
|
||||
const handleChange = () => {
|
||||
const handleChange = (checked: boolean) => {
|
||||
let updated: string[] = [];
|
||||
|
||||
if (option.id === "all") {
|
||||
updated = isAllChecked
|
||||
? []
|
||||
: options
|
||||
updated = checked
|
||||
? options
|
||||
.filter((opt: any) => opt.id !== "all")
|
||||
.map((opt: any) => opt.id);
|
||||
.map((opt: any) => opt.id)
|
||||
: [];
|
||||
} else {
|
||||
updated = isChecked
|
||||
? field.value.filter((val) => val !== option.id)
|
||||
: [...field.value, option.id];
|
||||
updated = checked
|
||||
? [...field.value, option.id]
|
||||
: field.value.filter((val) => val !== option.id);
|
||||
|
||||
if (isAllChecked && option.id !== "all") {
|
||||
updated = updated.filter((val) => val !== "all");
|
||||
|
|
@ -1587,17 +1617,47 @@ export default function FormAudio() {
|
|||
setPublishedFor(updated);
|
||||
};
|
||||
|
||||
// const handleChange = () => {
|
||||
// let updated: string[] = [];
|
||||
|
||||
// if (option.id === "all") {
|
||||
// updated = isAllChecked
|
||||
// ? []
|
||||
// : options
|
||||
// .filter((opt: any) => opt.id !== "all")
|
||||
// .map((opt: any) => opt.id);
|
||||
// } else {
|
||||
// updated = isChecked
|
||||
// ? field.value.filter((val) => val !== option.id)
|
||||
// : [...field.value, option.id];
|
||||
|
||||
// if (isAllChecked && option.id !== "all") {
|
||||
// updated = updated.filter((val) => val !== "all");
|
||||
// }
|
||||
// }
|
||||
|
||||
// field.onChange(updated);
|
||||
// setPublishedFor(updated);
|
||||
// };
|
||||
|
||||
return (
|
||||
<div
|
||||
key={option.id}
|
||||
className="flex gap-2 items-center"
|
||||
>
|
||||
<Checkbox
|
||||
<input
|
||||
type="checkbox"
|
||||
id={option.id}
|
||||
checked={isChecked}
|
||||
onChange={(e) => handleChange(e.target.checked)}
|
||||
className="h-4 w-4 border border-gray-300 rounded text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
{/* <Checkbox
|
||||
id={option.id}
|
||||
checked={isChecked}
|
||||
onCheckedChange={handleChange}
|
||||
className="border"
|
||||
/>
|
||||
/> */}
|
||||
<Label htmlFor={option.id}>{option.label}</Label>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -71,12 +71,12 @@ const audioSchema = z.object({
|
|||
"audio/x-wav",
|
||||
"audio/mp4",
|
||||
"audio/aac",
|
||||
].includes(file.type) && file.size <= 20 * 1024 * 1024
|
||||
].includes(file.type) && file.size <= 20 * 1024 * 1024,
|
||||
),
|
||||
{
|
||||
message:
|
||||
"Hanya file audio (.mp3, .wav, .m4a, .aac) dengan ukuran maksimal 20MB yang diperbolehkan.",
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
publishedFor: z
|
||||
|
|
@ -118,7 +118,7 @@ const CustomEditor = dynamic(
|
|||
() => {
|
||||
return import("@/components/editor/custom-editor");
|
||||
},
|
||||
{ ssr: false }
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
export default function FormAudioUpdate() {
|
||||
|
|
@ -232,7 +232,7 @@ export default function FormAudioUpdate() {
|
|||
|
||||
const handleEditTag = (index: number, newValue: string) => {
|
||||
setTags((prevTags) =>
|
||||
prevTags.map((tag, i) => (i === index ? newValue : tag))
|
||||
prevTags.map((tag, i) => (i === index ? newValue : tag)),
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -283,7 +283,7 @@ export default function FormAudioUpdate() {
|
|||
|
||||
const filesData = details.files || [];
|
||||
const fileUrls = filesData.map((file: { secondaryUrl: string }) =>
|
||||
file.secondaryUrl ? file.secondaryUrl : "default-image.jpg"
|
||||
file.secondaryUrl ? file.secondaryUrl : "default-image.jpg",
|
||||
);
|
||||
setDetailThumb(fileUrls);
|
||||
}
|
||||
|
|
@ -310,7 +310,7 @@ export default function FormAudioUpdate() {
|
|||
.filter(Boolean);
|
||||
|
||||
const allSelected = ["nasional", "wilayah", "internasional"].every((opt) =>
|
||||
options.includes(opt)
|
||||
options.includes(opt),
|
||||
);
|
||||
|
||||
return allSelected ? ["all", ...options] : options;
|
||||
|
|
@ -323,12 +323,12 @@ export default function FormAudioUpdate() {
|
|||
.filter((opt) => opt.id !== "all")
|
||||
.map((opt) => opt.id);
|
||||
setPublishedFor(
|
||||
publishedFor.length === allOptions.length ? [] : allOptions
|
||||
publishedFor.length === allOptions.length ? [] : allOptions,
|
||||
);
|
||||
} else {
|
||||
// Toggle individual option
|
||||
setPublishedFor((prev) =>
|
||||
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]
|
||||
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id],
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -352,20 +352,31 @@ export default function FormAudioUpdate() {
|
|||
// isYoutube: false,
|
||||
// isInternationalMedia: false,
|
||||
// };
|
||||
|
||||
const payload = {
|
||||
aiArticleId: detail?.aiArticleId ?? "",
|
||||
categoryIds: selectedCategory ?? "",
|
||||
createdById: detail?.createdById ?? "",
|
||||
aiArticleId: detail.aiArticleId,
|
||||
categoryIds: selectedCategory,
|
||||
description: htmlToString(data.description),
|
||||
htmlDescription: data.description,
|
||||
isDraft: false,
|
||||
isPublish: true,
|
||||
slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"),
|
||||
statusId: detail?.statusId ?? 1,
|
||||
tags: tags.join(","),
|
||||
title: data.title,
|
||||
typeId: detail?.typeId ?? 4,
|
||||
typeId: detail.typeId,
|
||||
slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"),
|
||||
};
|
||||
// const payload = {
|
||||
// aiArticleId: detail?.aiArticleId ?? "",
|
||||
// categoryIds: selectedCategory ?? "",
|
||||
// createdById: detail?.createdById ?? "",
|
||||
// description: htmlToString(data.description),
|
||||
// htmlDescription: data.description,
|
||||
// isDraft: false,
|
||||
// isPublish: true,
|
||||
// slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"),
|
||||
// statusId: detail?.statusId ?? 1,
|
||||
// tags: tags.join(","),
|
||||
// title: data.title,
|
||||
// typeId: detail?.typeId ?? 4,
|
||||
// };
|
||||
|
||||
const res = await updateArticle(Number(id), payload);
|
||||
if (res?.error) {
|
||||
|
|
@ -421,7 +432,7 @@ export default function FormAudioUpdate() {
|
|||
idx: number,
|
||||
id: string,
|
||||
file: any,
|
||||
duration: string
|
||||
duration: string,
|
||||
) {
|
||||
console.log(idx, id, file, duration);
|
||||
|
||||
|
|
@ -458,7 +469,7 @@ export default function FormAudioUpdate() {
|
|||
onChunkComplete: (
|
||||
chunkSize: any,
|
||||
bytesAccepted: any,
|
||||
bytesTotal: any
|
||||
bytesTotal: any,
|
||||
) => {
|
||||
const uploadPersen = Math.floor((bytesAccepted / bytesTotal) * 100);
|
||||
progressInfo[idx].percentage = uploadPersen;
|
||||
|
|
@ -614,7 +625,7 @@ export default function FormAudioUpdate() {
|
|||
|
||||
// If all individual options are selected, include "all" automatically
|
||||
const isAllSelected = ["nasional", "wilayah", "internasional"].every(
|
||||
(opt) => updatedSelections.includes(opt)
|
||||
(opt) => updatedSelections.includes(opt),
|
||||
);
|
||||
return {
|
||||
...prev,
|
||||
|
|
@ -667,7 +678,7 @@ export default function FormAudioUpdate() {
|
|||
|
||||
// Jika berhasil, hapus file dari state lokal
|
||||
setFiles((prevFiles: any) =>
|
||||
prevFiles.filter((file: any) => file.id !== id)
|
||||
prevFiles.filter((file: any) => file.id !== id),
|
||||
);
|
||||
success();
|
||||
} catch (err) {
|
||||
|
|
@ -812,7 +823,7 @@ export default function FormAudioUpdate() {
|
|||
) : (
|
||||
<>
|
||||
{(Math.round(file.size / 100) / 10).toFixed(
|
||||
1
|
||||
1,
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
|
@ -1029,13 +1040,13 @@ export default function FormAudioUpdate() {
|
|||
} else {
|
||||
updated = isChecked
|
||||
? field.value.filter(
|
||||
(val) => val !== option.id
|
||||
(val) => val !== option.id,
|
||||
)
|
||||
: [...field.value, option.id];
|
||||
|
||||
if (isAllChecked && option.id !== "all") {
|
||||
updated = updated.filter(
|
||||
(val) => val !== "all"
|
||||
(val) => val !== "all",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ const CustomEditor = dynamic(
|
|||
() => {
|
||||
return import("@/components/editor/custom-editor");
|
||||
},
|
||||
{ ssr: false }
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
type Option = {
|
||||
|
|
@ -114,7 +114,7 @@ export default function FormTeks() {
|
|||
const [isGeneratedArticle, setIsGeneratedArticle] = useState(false);
|
||||
const [articleBody, setArticleBody] = useState<string>("");
|
||||
const [selectedArticleId, setSelectedArticleId] = useState<string | null>(
|
||||
null
|
||||
null,
|
||||
);
|
||||
const [isContentRewriteClicked, setIsContentRewriteClicked] = useState(false);
|
||||
const [showRewriteEditor, setShowRewriteEditor] = useState(false);
|
||||
|
|
@ -168,13 +168,13 @@ export default function FormTeks() {
|
|||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/vnd.ms-powerpoint",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
].includes(file.type) && file.size <= 20 * 1024 * 1024
|
||||
].includes(file.type) && file.size <= 20 * 1024 * 1024,
|
||||
);
|
||||
|
||||
const filesWithPreview = filtered.map((file) =>
|
||||
Object.assign(file, {
|
||||
preview: URL.createObjectURL(file),
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
setFiles(filesWithPreview);
|
||||
|
|
@ -200,12 +200,12 @@ export default function FormTeks() {
|
|||
"application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"text/plain",
|
||||
].includes(file.type) && file.size <= 100 * 1024 * 1024
|
||||
].includes(file.type) && file.size <= 100 * 1024 * 1024,
|
||||
),
|
||||
{
|
||||
message:
|
||||
"Hanya file .pdf, .doc, .docx, .txt, maksimal 100MB yang diperbolehkan.",
|
||||
}
|
||||
},
|
||||
),
|
||||
categoryId: z.string().min(1, { message: "Kategori wajib dipilih." }),
|
||||
tags: z
|
||||
|
|
@ -420,7 +420,7 @@ export default function FormTeks() {
|
|||
const articleData = await waitForStatusUpdate();
|
||||
const cleanArticleBody = articleData?.articleBody?.replace(
|
||||
/<img[^>]*>/g,
|
||||
""
|
||||
"",
|
||||
);
|
||||
const articleImagesData = articleData?.imagesUrl?.split(",");
|
||||
setArticleBody(cleanArticleBody || "");
|
||||
|
|
@ -500,7 +500,7 @@ export default function FormTeks() {
|
|||
|
||||
if (scheduleId && scheduleType === "3") {
|
||||
const findCategory = resCategory.find((o) =>
|
||||
o.name.toLowerCase().includes("pers rilis")
|
||||
o.name.toLowerCase().includes("pers rilis"),
|
||||
);
|
||||
|
||||
if (findCategory) {
|
||||
|
|
@ -533,7 +533,7 @@ export default function FormTeks() {
|
|||
setPublishedFor(
|
||||
options
|
||||
.filter((opt: any) => opt.id !== "all")
|
||||
.map((opt: any) => opt.id)
|
||||
.map((opt: any) => opt.id),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -572,8 +572,8 @@ export default function FormTeks() {
|
|||
const finalDescription = isSwitchOn
|
||||
? data.description
|
||||
: selectedFileType === "rewrite"
|
||||
? data.rewriteDescription
|
||||
: data.descriptionOri;
|
||||
? data.rewriteDescription
|
||||
: data.descriptionOri;
|
||||
|
||||
if (!finalDescription?.trim()) {
|
||||
MySwal.fire("Error", "Deskripsi tidak boleh kosong.", "error");
|
||||
|
|
@ -637,10 +637,10 @@ export default function FormTeks() {
|
|||
|
||||
if (id == undefined) {
|
||||
const articleData: CreateArticleData = {
|
||||
aiArticleId: 0,
|
||||
aiArticleId: 0,
|
||||
categoryIds: selectedCategory.toString(),
|
||||
createdAt: formatDateForBackend(new Date()),
|
||||
createdById: Number(userId),
|
||||
createdAt: formatDateForBackend(new Date()),
|
||||
createdById: Number(userId),
|
||||
description: htmlToString(finalDescription),
|
||||
htmlDescription: finalDescription,
|
||||
isDraft: true,
|
||||
|
|
@ -652,7 +652,7 @@ export default function FormTeks() {
|
|||
.replace(/[^a-z0-9-]/g, ""),
|
||||
tags: finalTags,
|
||||
title: finalTitle,
|
||||
typeId: 3,
|
||||
typeId: 3,
|
||||
};
|
||||
const response = await createArticle(articleData);
|
||||
console.log("Article Data Submitted:", articleData);
|
||||
|
|
@ -662,7 +662,7 @@ export default function FormTeks() {
|
|||
MySwal.fire(
|
||||
"Error",
|
||||
response.message || "Failed to create article",
|
||||
"error"
|
||||
"error",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -686,7 +686,7 @@ export default function FormTeks() {
|
|||
MySwal.fire(
|
||||
"Error",
|
||||
uploadResponse.message || "Failed to upload files",
|
||||
"error"
|
||||
"error",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -702,18 +702,18 @@ export default function FormTeks() {
|
|||
try {
|
||||
const thumbnailResponse = await uploadArticleThumbnail(
|
||||
articleId,
|
||||
thumbnailFormData
|
||||
thumbnailFormData,
|
||||
);
|
||||
|
||||
if (thumbnailResponse?.error) {
|
||||
console.warn(
|
||||
"Thumbnail upload failed:",
|
||||
thumbnailResponse.message
|
||||
thumbnailResponse.message,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
"Thumbnail uploaded successfully:",
|
||||
thumbnailResponse
|
||||
thumbnailResponse,
|
||||
);
|
||||
}
|
||||
} catch (thumbnailError) {
|
||||
|
|
@ -725,7 +725,7 @@ export default function FormTeks() {
|
|||
MySwal.fire(
|
||||
"Error",
|
||||
"Failed to upload files. Please try again.",
|
||||
"error"
|
||||
"error",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -767,7 +767,7 @@ export default function FormTeks() {
|
|||
idx: number,
|
||||
id: string,
|
||||
file: any,
|
||||
duration: string
|
||||
duration: string,
|
||||
) {
|
||||
console.log(idx, id, file, duration);
|
||||
|
||||
|
|
@ -804,7 +804,7 @@ export default function FormTeks() {
|
|||
onChunkComplete: (
|
||||
chunkSize: any,
|
||||
bytesAccepted: any,
|
||||
bytesTotal: any
|
||||
bytesTotal: any,
|
||||
) => {
|
||||
const uploadPersen = Math.floor((bytesAccepted / bytesTotal) * 100);
|
||||
progressInfo[idx].percentage = uploadPersen;
|
||||
|
|
@ -1036,6 +1036,36 @@ export default function FormTeks() {
|
|||
<div className="flex flex-row items-center gap-3 py-2">
|
||||
<Label>Ai Assistance</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSwitchOn}
|
||||
onChange={(e) => setIsSwitchOn(e.target.checked)}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div
|
||||
className="
|
||||
w-11 h-6
|
||||
bg-gray-300
|
||||
rounded-full
|
||||
peer
|
||||
peer-checked:bg-blue-600
|
||||
transition-colors
|
||||
after:content-['']
|
||||
after:absolute
|
||||
after:top-[2px]
|
||||
after:left-[2px]
|
||||
after:bg-white
|
||||
after:rounded-full
|
||||
after:h-5
|
||||
after:w-5
|
||||
after:transition-transform
|
||||
peer-checked:after:translate-x-5
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{/* <div className="flex items-center gap-3">
|
||||
<Switch
|
||||
defaultChecked={isSwitchOn}
|
||||
color="primary"
|
||||
|
|
@ -1044,7 +1074,7 @@ export default function FormTeks() {
|
|||
setIsSwitchOn(checked)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
{isSwitchOn && (
|
||||
<div>
|
||||
|
|
@ -1517,19 +1547,42 @@ export default function FormTeks() {
|
|||
? isAllChecked
|
||||
: field.value.includes(option.id);
|
||||
|
||||
const handleChange = () => {
|
||||
// const handleChange = () => {
|
||||
// let updated: string[] = [];
|
||||
|
||||
// if (option.id === "all") {
|
||||
// updated = isAllChecked
|
||||
// ? []
|
||||
// : options
|
||||
// .filter((opt: any) => opt.id !== "all")
|
||||
// .map((opt: any) => opt.id);
|
||||
// } else {
|
||||
// updated = isChecked
|
||||
// ? field.value.filter((val) => val !== option.id)
|
||||
// : [...field.value, option.id];
|
||||
|
||||
// if (isAllChecked && option.id !== "all") {
|
||||
// updated = updated.filter((val) => val !== "all");
|
||||
// }
|
||||
// }
|
||||
|
||||
// field.onChange(updated);
|
||||
// setPublishedFor(updated);
|
||||
// };
|
||||
|
||||
const handleChange = (checked: boolean) => {
|
||||
let updated: string[] = [];
|
||||
|
||||
if (option.id === "all") {
|
||||
updated = isAllChecked
|
||||
? []
|
||||
: options
|
||||
updated = checked
|
||||
? options
|
||||
.filter((opt: any) => opt.id !== "all")
|
||||
.map((opt: any) => opt.id);
|
||||
.map((opt: any) => opt.id)
|
||||
: [];
|
||||
} else {
|
||||
updated = isChecked
|
||||
? field.value.filter((val) => val !== option.id)
|
||||
: [...field.value, option.id];
|
||||
updated = checked
|
||||
? [...field.value, option.id]
|
||||
: field.value.filter((val) => val !== option.id);
|
||||
|
||||
if (isAllChecked && option.id !== "all") {
|
||||
updated = updated.filter((val) => val !== "all");
|
||||
|
|
@ -1545,12 +1598,19 @@ export default function FormTeks() {
|
|||
key={option.id}
|
||||
className="flex gap-2 items-center"
|
||||
>
|
||||
<Checkbox
|
||||
<input
|
||||
type="checkbox"
|
||||
id={option.id}
|
||||
checked={isChecked}
|
||||
onChange={(e) => handleChange(e.target.checked)}
|
||||
className="h-4 w-4 border border-gray-300 rounded text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
{/* <Checkbox
|
||||
id={option.id}
|
||||
checked={isChecked}
|
||||
onCheckedChange={handleChange}
|
||||
className="border"
|
||||
/>
|
||||
/> */}
|
||||
<Label htmlFor={option.id}>{option.label}</Label>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -70,12 +70,12 @@ const teksSchema = z.object({
|
|||
"application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"text/plain",
|
||||
].includes(file.type) && file.size <= 10 * 1024 * 1024
|
||||
].includes(file.type) && file.size <= 10 * 1024 * 1024,
|
||||
),
|
||||
{
|
||||
message:
|
||||
"Hanya file .pdf, .doc, .docx, atau .txt dengan ukuran maksimal 10MB yang diperbolehkan.",
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
publishedFor: z
|
||||
|
|
@ -121,7 +121,7 @@ const CustomEditor = dynamic(
|
|||
() => {
|
||||
return import("@/components/editor/custom-editor");
|
||||
},
|
||||
{ ssr: false }
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
export default function FormTeksUpdate() {
|
||||
|
|
@ -273,7 +273,7 @@ export default function FormTeksUpdate() {
|
|||
.filter(Boolean);
|
||||
|
||||
const allSelected = ["nasional", "wilayah", "internasional"].every((opt) =>
|
||||
options.includes(opt)
|
||||
options.includes(opt),
|
||||
);
|
||||
|
||||
return allSelected ? ["all", ...options] : options;
|
||||
|
|
@ -286,12 +286,12 @@ export default function FormTeksUpdate() {
|
|||
.filter((opt) => opt.id !== "all")
|
||||
.map((opt) => opt.id);
|
||||
setPublishedFor(
|
||||
publishedFor.length === allOptions.length ? [] : allOptions
|
||||
publishedFor.length === allOptions.length ? [] : allOptions,
|
||||
);
|
||||
} else {
|
||||
// Toggle individual option
|
||||
setPublishedFor((prev) =>
|
||||
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]
|
||||
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id],
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -316,20 +316,31 @@ export default function FormTeksUpdate() {
|
|||
// isYoutube: false,
|
||||
// isInternationalMedia: false,
|
||||
// };
|
||||
|
||||
const payload = {
|
||||
aiArticleId: detail?.aiArticleId ?? "",
|
||||
aiArticleId: detail.aiArticleId,
|
||||
categoryIds: selectedCategory,
|
||||
createdById: detail?.createdById ?? "",
|
||||
description: htmlToString(data.description),
|
||||
htmlDescription: data.description,
|
||||
isDraft: false,
|
||||
isPublish: true,
|
||||
slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"),
|
||||
statusId: detail?.statusId ?? 1,
|
||||
tags: tags.join(","),
|
||||
title: data.title,
|
||||
typeId: detail?.typeId ?? 3,
|
||||
typeId: detail.typeId,
|
||||
slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"),
|
||||
};
|
||||
// const payload = {
|
||||
// aiArticleId: detail?.aiArticleId ?? "",
|
||||
// categoryIds: selectedCategory,
|
||||
// createdById: detail?.createdById ?? "",
|
||||
// description: htmlToString(data.description),
|
||||
// htmlDescription: data.description,
|
||||
// isDraft: false,
|
||||
// isPublish: true,
|
||||
// slug: detail?.slug ?? data.title.toLowerCase().replace(/\s+/g, "-"),
|
||||
// statusId: detail?.statusId ?? 1,
|
||||
// tags: tags.join(","),
|
||||
// title: data.title,
|
||||
// typeId: detail?.typeId ?? 3,
|
||||
// };
|
||||
|
||||
const res = await updateArticle(Number(id), payload);
|
||||
if (res?.error) {
|
||||
|
|
@ -386,7 +397,7 @@ export default function FormTeksUpdate() {
|
|||
idx: number,
|
||||
id: string,
|
||||
file: any,
|
||||
duration: string
|
||||
duration: string,
|
||||
) {
|
||||
console.log(idx, id, file, duration);
|
||||
|
||||
|
|
@ -424,7 +435,7 @@ export default function FormTeksUpdate() {
|
|||
onChunkComplete: (
|
||||
chunkSize: any,
|
||||
bytesAccepted: any,
|
||||
bytesTotal: any
|
||||
bytesTotal: any,
|
||||
) => {
|
||||
const uploadPersen = Math.floor((bytesAccepted / bytesTotal) * 100);
|
||||
progressInfo[idx].percentage = uploadPersen;
|
||||
|
|
@ -568,7 +579,7 @@ export default function FormTeksUpdate() {
|
|||
|
||||
// If all individual options are selected, include "all" automatically
|
||||
const isAllSelected = ["nasional", "wilayah", "internasional"].every(
|
||||
(opt) => updatedSelections.includes(opt)
|
||||
(opt) => updatedSelections.includes(opt),
|
||||
);
|
||||
return {
|
||||
...prev,
|
||||
|
|
@ -599,7 +610,7 @@ export default function FormTeksUpdate() {
|
|||
|
||||
const handleEditTag = (index: number, newValue: string) => {
|
||||
setTags((prevTags) =>
|
||||
prevTags.map((tag, i) => (i === index ? newValue : tag))
|
||||
prevTags.map((tag, i) => (i === index ? newValue : tag)),
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -692,12 +703,12 @@ export default function FormTeksUpdate() {
|
|||
<Fragment>
|
||||
<div>{fileList}</div>
|
||||
<div className=" flex justify-between gap-2">
|
||||
<div className="flex flex-row items-center gap-3 py-3">
|
||||
{/* <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>
|
||||
</div> */}
|
||||
<Button
|
||||
color="destructive"
|
||||
onClick={handleRemoveAllFiles}
|
||||
|
|
@ -708,111 +719,112 @@ export default function FormTeksUpdate() {
|
|||
</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>
|
||||
<></>
|
||||
// <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>
|
||||
|
|
@ -914,13 +926,13 @@ export default function FormTeksUpdate() {
|
|||
} else {
|
||||
updated = isChecked
|
||||
? field.value.filter(
|
||||
(val) => val !== option.id
|
||||
(val) => val !== option.id,
|
||||
)
|
||||
: [...field.value, option.id];
|
||||
|
||||
if (isAllChecked && option.id !== "all") {
|
||||
updated = updated.filter(
|
||||
(val) => val !== "all"
|
||||
(val) => val !== "all",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1119,6 +1119,37 @@ export default function FormImageDetail() {
|
|||
</DialogContent>
|
||||
</Dialog>
|
||||
</Card>
|
||||
|
||||
{/* {(Number(detail.needApprovalFromLevel || 0) ==
|
||||
Number(userLevelId) ||
|
||||
(detail.isPublish === false && detail.statusId == 1)) &&
|
||||
Number(detail.uploadedById || detail.createdById) !=
|
||||
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>
|
||||
) : null} */}
|
||||
|
||||
{/* {Number(detail?.needApprovalFromLevel || 0) ==
|
||||
Number(userLevelId) ||
|
||||
(detail?.isInternationalMedia == true &&
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ const UserInternalTable = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-black">
|
||||
<div className="bg-white">
|
||||
<div className="flex justify-between py-3">
|
||||
<Input
|
||||
type="text"
|
||||
|
|
|
|||
|
|
@ -1,338 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { useRouter } from "@/components/navigation";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
LoginFormData,
|
||||
ProfileData,
|
||||
AuthState,
|
||||
AuthContextType,
|
||||
EmailValidationData,
|
||||
OTPData,
|
||||
} from "@/types/auth";
|
||||
import {
|
||||
login,
|
||||
getProfile,
|
||||
postEmailValidation,
|
||||
postSetupEmail,
|
||||
verifyOTPByUsername,
|
||||
doLogin,
|
||||
} from "@/service/auth";
|
||||
import {
|
||||
setAuthCookies,
|
||||
setProfileCookies,
|
||||
clearAllCookies,
|
||||
isUserEligible,
|
||||
isProtectedRole,
|
||||
getNavigationPath,
|
||||
showAuthError,
|
||||
showAuthSuccess,
|
||||
loginRateLimiter,
|
||||
AUTH_CONSTANTS,
|
||||
} from "@/lib/auth-utils";
|
||||
import { warning } from "@/lib/swal";
|
||||
|
||||
export const useAuth = (): AuthContextType => {
|
||||
const router = useRouter();
|
||||
const [state, setState] = useState<AuthState>({
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
// Check if user is authenticated on mount
|
||||
useEffect(() => {
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
setState((prev) => ({ ...prev, loading: true }));
|
||||
// Add logic to check if user is authenticated
|
||||
// This could check for valid tokens, etc.
|
||||
} catch (error) {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
error: "Authentication check failed",
|
||||
}));
|
||||
} finally {
|
||||
setState((prev) => ({ ...prev, loading: false }));
|
||||
}
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
}, []);
|
||||
|
||||
const login = useCallback(
|
||||
async (credentials: LoginFormData): Promise<void> => {
|
||||
try {
|
||||
setState((prev) => ({ ...prev, loading: true, error: null }));
|
||||
|
||||
// Check rate limiting
|
||||
if (!loginRateLimiter.canAttempt(credentials.username)) {
|
||||
const remainingTime = loginRateLimiter.getRemainingTime(
|
||||
credentials.username
|
||||
);
|
||||
const minutes = Math.ceil(remainingTime / (60 * 1000));
|
||||
throw new Error(
|
||||
`Too many login attempts. Please try again in ${minutes} minutes.`
|
||||
);
|
||||
}
|
||||
|
||||
// Attempt login
|
||||
const response = await doLogin({
|
||||
...credentials,
|
||||
grantType: AUTH_CONSTANTS.GRANT_TYPE,
|
||||
clientId: AUTH_CONSTANTS.CLIENT_ID,
|
||||
});
|
||||
|
||||
if (response?.error) {
|
||||
loginRateLimiter.recordAttempt(credentials.username);
|
||||
throw new Error("Invalid username or password");
|
||||
}
|
||||
|
||||
const { access_token, refresh_token } = response?.data || {};
|
||||
|
||||
if (!access_token || !refresh_token) {
|
||||
throw new Error("Invalid response from server");
|
||||
}
|
||||
|
||||
// Set auth cookies
|
||||
setAuthCookies(access_token, refresh_token);
|
||||
|
||||
// Get user profile
|
||||
const profileResponse = await getProfile(access_token);
|
||||
const profile: ProfileData = profileResponse?.data?.data;
|
||||
|
||||
if (!profile) {
|
||||
throw new Error("Failed to fetch user profile");
|
||||
}
|
||||
|
||||
// Validate user eligibility
|
||||
// if (!isUserEligible(profile)) {
|
||||
// clearAllCookies();
|
||||
// warning(
|
||||
// "Akun Anda tidak dapat digunakan untuk masuk ke MediaHub Polri",
|
||||
// "/auth"
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Set profile cookies
|
||||
setProfileCookies(profile);
|
||||
|
||||
// Reset rate limiter on successful login
|
||||
loginRateLimiter.resetAttempts(credentials.username);
|
||||
|
||||
// Navigate based on user role
|
||||
const navigationPath = getNavigationPath(
|
||||
profile.roleId,
|
||||
profile.userLevel?.id,
|
||||
profile.userLevel?.parentLevelId
|
||||
);
|
||||
|
||||
// Update state
|
||||
setState({
|
||||
isAuthenticated: true,
|
||||
user: profile,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
// Navigate to appropriate dashboard
|
||||
window.location.href = navigationPath;
|
||||
} catch (error: any) {
|
||||
const errorMessage = error?.message || "Login failed";
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: errorMessage,
|
||||
}));
|
||||
showAuthError(error, "Login failed");
|
||||
}
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const logout = useCallback((): void => {
|
||||
clearAllCookies();
|
||||
setState({
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
router.push("/auth");
|
||||
}, [router]);
|
||||
|
||||
const refreshToken = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
setState((prev) => ({ ...prev, loading: true }));
|
||||
// Add token refresh logic here
|
||||
// This would typically call an API to refresh the access token
|
||||
} catch (error) {
|
||||
logout();
|
||||
} finally {
|
||||
setState((prev) => ({ ...prev, loading: false }));
|
||||
}
|
||||
}, [logout]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
login,
|
||||
logout,
|
||||
refreshToken,
|
||||
};
|
||||
};
|
||||
|
||||
// Hook for email validation step
|
||||
export const useEmailValidation = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const validateEmail = useCallback(
|
||||
async (credentials: LoginFormData): Promise<string> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await postEmailValidation(credentials);
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error(response?.message || "Email validation failed");
|
||||
}
|
||||
|
||||
const message = response?.data?.message;
|
||||
|
||||
switch (message) {
|
||||
case "Continue to setup email":
|
||||
return "setup";
|
||||
case "Email is valid and OTP has been sent":
|
||||
return "otp";
|
||||
case "Username & password valid":
|
||||
return "success";
|
||||
case "Skip to login":
|
||||
return "skip";
|
||||
default:
|
||||
return "login";
|
||||
}
|
||||
} catch (error: any) {
|
||||
const errorMessage = error?.message || "Email validation failed";
|
||||
setError(errorMessage);
|
||||
showAuthError(error, "Email validation failed");
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
validateEmail,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
// Hook for email setup step
|
||||
export const useEmailSetup = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const setupEmail = useCallback(
|
||||
async (
|
||||
credentials: LoginFormData,
|
||||
emailData: EmailValidationData
|
||||
): Promise<string> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const data = {
|
||||
username: credentials.username,
|
||||
password: credentials.password,
|
||||
oldEmail: emailData.oldEmail,
|
||||
newEmail: emailData.newEmail,
|
||||
};
|
||||
|
||||
const response = await postSetupEmail(data);
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error(response.message || "Email setup failed");
|
||||
}
|
||||
|
||||
const message = response?.data?.message;
|
||||
|
||||
switch (message) {
|
||||
case "Email is valid and OTP has been sent":
|
||||
return "otp";
|
||||
case "The old email is not same":
|
||||
throw new Error("Email is invalid");
|
||||
default:
|
||||
return "success";
|
||||
}
|
||||
} catch (error: any) {
|
||||
const errorMessage = error?.message || "Email setup failed";
|
||||
setError(errorMessage);
|
||||
showAuthError(error, "Email setup failed");
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
setupEmail,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
// Hook for OTP verification
|
||||
export const useOTPVerification = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const verifyOTP = useCallback(
|
||||
async (username: string, otp: string): Promise<boolean> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
if (otp.length !== 6) {
|
||||
throw new Error("OTP must be exactly 6 digits");
|
||||
}
|
||||
|
||||
const data = {
|
||||
username: username,
|
||||
otpCode: otp,
|
||||
}
|
||||
const response = await verifyOTPByUsername(data);
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error(response.message || "OTP verification failed");
|
||||
}
|
||||
|
||||
return response?.message === "success";
|
||||
} catch (error: any) {
|
||||
const errorMessage = error?.message || "OTP verification failed";
|
||||
setError(errorMessage);
|
||||
showAuthError(error, "OTP verification failed");
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
verifyOTP,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
|
||||
import { atom, useAtom } from "jotai";
|
||||
|
||||
export type ChatConfig = {
|
||||
isOpen: boolean;
|
||||
showInfo: boolean;
|
||||
showProfile: boolean;
|
||||
};
|
||||
|
||||
const chatConfigAtom = atom<ChatConfig>({
|
||||
isOpen: false,
|
||||
showInfo: false,
|
||||
showProfile: false,
|
||||
});
|
||||
|
||||
export function useChatConfig() {
|
||||
return useAtom(chatConfigAtom);
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
import { useAtom } from "jotai"
|
||||
import { atomWithStorage } from "jotai/utils"
|
||||
import { layoutType, sidebarType, navBarType} from "@/lib/type"
|
||||
|
||||
|
||||
|
||||
export type Config = {
|
||||
collapsed: boolean
|
||||
theme: string
|
||||
skin: 'default' | 'bordered'
|
||||
layout: layoutType
|
||||
sidebar: sidebarType
|
||||
menuHidden: boolean,
|
||||
showSearchBar: boolean,
|
||||
showSwitcher: boolean
|
||||
topHeader: 'default' | 'links'
|
||||
contentWidth: 'wide' | 'boxed'
|
||||
navbar: navBarType
|
||||
footer: 'sticky' | 'default' | 'hidden'
|
||||
isRtl: boolean
|
||||
subMenu: boolean
|
||||
hasSubMenu: boolean
|
||||
sidebarTheme: string,
|
||||
headerTheme: string,
|
||||
sidebarBgImage?: string
|
||||
radius: number
|
||||
|
||||
}
|
||||
export const defaultConfig: Config = {
|
||||
collapsed: false,
|
||||
theme: "zinc",
|
||||
skin: 'default',
|
||||
layout: "vertical",
|
||||
sidebar: 'classic',
|
||||
menuHidden: false,
|
||||
showSearchBar: true,
|
||||
topHeader: 'default',
|
||||
contentWidth: 'wide',
|
||||
navbar: 'sticky',
|
||||
footer: 'default',
|
||||
isRtl: false,
|
||||
showSwitcher: true,
|
||||
subMenu: false,
|
||||
hasSubMenu: false,
|
||||
sidebarTheme: 'dark',
|
||||
headerTheme: 'light',
|
||||
sidebarBgImage: undefined,
|
||||
radius: 0.5,
|
||||
}
|
||||
|
||||
|
||||
const configAtom = atomWithStorage<Config>("config", defaultConfig)
|
||||
|
||||
export function useConfig() {
|
||||
|
||||
return useAtom(configAtom)
|
||||
}
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
import { useEffect, useState, useCallback } from 'react';
|
||||
import {
|
||||
FacebookLoginResponse,
|
||||
FacebookLoginError,
|
||||
FacebookUser,
|
||||
FacebookSDKInitOptions
|
||||
} from '@/types/facebook-login';
|
||||
|
||||
export interface UseFacebookLoginOptions extends FacebookSDKInitOptions {}
|
||||
|
||||
export const useFacebookLogin = (options: UseFacebookLoginOptions) => {
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
const [user, setUser] = useState<FacebookUser | null>(null);
|
||||
|
||||
const { appId, version = 'v18.0', cookie = true, xfbml = true, autoLogAppEvents = true } = options;
|
||||
|
||||
// Initialize Facebook SDK
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
// Load Facebook SDK if not already loaded
|
||||
if (!window.FB) {
|
||||
const script = document.createElement('script');
|
||||
script.src = `https://connect.facebook.net/en_US/sdk.js`;
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
script.crossOrigin = 'anonymous';
|
||||
|
||||
window.fbAsyncInit = () => {
|
||||
window.FB.init({
|
||||
appId,
|
||||
cookie,
|
||||
xfbml,
|
||||
version,
|
||||
autoLogAppEvents,
|
||||
});
|
||||
setIsLoaded(true);
|
||||
|
||||
// Check login status
|
||||
window.FB.getLoginStatus((response: any) => {
|
||||
if (response.status === 'connected') {
|
||||
setIsLoggedIn(true);
|
||||
getUserInfo(response.authResponse.accessToken);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
document.head.appendChild(script);
|
||||
} else {
|
||||
setIsLoaded(true);
|
||||
}
|
||||
|
||||
return () => {
|
||||
// Cleanup if needed
|
||||
};
|
||||
}, [appId, cookie, xfbml, version, autoLogAppEvents]);
|
||||
|
||||
const getUserInfo = useCallback((accessToken: string) => {
|
||||
window.FB.api('/me', { fields: 'name,email,picture' }, (response: FacebookUser) => {
|
||||
if (response && !response.error) {
|
||||
setUser(response);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const login = useCallback((permissions: string[] = ['public_profile', 'email']) => {
|
||||
return new Promise<FacebookLoginResponse>((resolve, reject) => {
|
||||
if (!window.FB) {
|
||||
reject(new Error('Facebook SDK not loaded'));
|
||||
return;
|
||||
}
|
||||
|
||||
window.FB.login((response: any) => {
|
||||
if (response.status === 'connected') {
|
||||
setIsLoggedIn(true);
|
||||
getUserInfo(response.authResponse.accessToken);
|
||||
resolve(response.authResponse);
|
||||
} else if (response.status === 'not_authorized') {
|
||||
reject({ error: 'not_authorized', errorDescription: 'User denied permissions' });
|
||||
} else {
|
||||
reject({ error: 'unknown', errorDescription: 'Login failed' });
|
||||
}
|
||||
}, { scope: permissions.join(',') });
|
||||
});
|
||||
}, [getUserInfo]);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (!window.FB) {
|
||||
reject(new Error('Facebook SDK not loaded'));
|
||||
return;
|
||||
}
|
||||
|
||||
window.FB.logout((response: any) => {
|
||||
setIsLoggedIn(false);
|
||||
setUser(null);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
const getLoginStatus = useCallback(() => {
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
if (!window.FB) {
|
||||
reject(new Error('Facebook SDK not loaded'));
|
||||
return;
|
||||
}
|
||||
|
||||
window.FB.getLoginStatus((response: any) => {
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isLoaded,
|
||||
isLoggedIn,
|
||||
user,
|
||||
login,
|
||||
logout,
|
||||
getLoginStatus,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import { useAtom } from "jotai";
|
||||
import { atomWithStorage } from "jotai/utils";
|
||||
|
||||
export type MailConfig = {
|
||||
isOpen: boolean;
|
||||
};
|
||||
|
||||
const mailConfigAtom = atomWithStorage<MailConfig>("mailConfig", {
|
||||
isOpen: false,
|
||||
});
|
||||
|
||||
export function useMailConfig() {
|
||||
return useAtom(mailConfigAtom);
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import * as React from "react"
|
||||
|
||||
export function useMediaQuery(query: string) {
|
||||
const [value, setValue] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
function onChange(event: MediaQueryListEvent) {
|
||||
setValue(event.matches)
|
||||
}
|
||||
|
||||
const result = matchMedia(query)
|
||||
result.addEventListener("change", onChange)
|
||||
setValue(result.matches)
|
||||
|
||||
return () => result.removeEventListener("change", onChange)
|
||||
}, [query])
|
||||
|
||||
return value
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
|
||||
import { atom, useAtom } from "jotai";
|
||||
|
||||
export type hoverConfig = {
|
||||
hovered: boolean;
|
||||
|
||||
};
|
||||
|
||||
const menuHoverConfigAtom = atom<hoverConfig>({
|
||||
hovered: false,
|
||||
|
||||
});
|
||||
|
||||
export function useMenuHoverConfig() {
|
||||
return useAtom(menuHoverConfigAtom);
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
|
||||
import { atom, useAtom } from "jotai";
|
||||
|
||||
export type MobileMenuConfig = {
|
||||
isOpen: boolean;
|
||||
};
|
||||
|
||||
const mobileMenuConfig = atom<MobileMenuConfig>({
|
||||
isOpen: false,
|
||||
});
|
||||
|
||||
export function useMobileMenuConfig() {
|
||||
return useAtom(mobileMenuConfig);
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import * as React from "react"
|
||||
|
||||
export function useMounted() {
|
||||
const [mounted, setMounted] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
return mounted
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import * as React from "react"
|
||||
|
||||
export const useMutationObserver = (
|
||||
ref: React.MutableRefObject<HTMLElement | null>,
|
||||
callback: MutationCallback,
|
||||
options = {
|
||||
attributes: true,
|
||||
characterData: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
}
|
||||
) => {
|
||||
React.useEffect(() => {
|
||||
if (ref.current) {
|
||||
const observer = new MutationObserver(callback)
|
||||
observer.observe(ref.current, options)
|
||||
return () => observer.disconnect()
|
||||
}
|
||||
}, [ref, callback, options])
|
||||
}
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useCallback } from "react";
|
||||
|
||||
// Extend Window interface for gtag
|
||||
declare global {
|
||||
interface Window {
|
||||
gtag?: (command: string, targetId: string, config: any) => void;
|
||||
}
|
||||
}
|
||||
|
||||
interface PerformanceMetrics {
|
||||
FCP: number | null;
|
||||
LCP: number | null;
|
||||
FID: number | null;
|
||||
CLS: number | null;
|
||||
TTFB: number | null;
|
||||
}
|
||||
|
||||
export const usePerformance = () => {
|
||||
const reportMetric = useCallback((name: string, value: number) => {
|
||||
// Send to analytics service
|
||||
if (typeof window !== "undefined" && window.gtag) {
|
||||
window.gtag("event", name, {
|
||||
value: Math.round(name === "CLS" ? value * 1000 : value),
|
||||
metric_id: name,
|
||||
metric_value: value,
|
||||
metric_delta: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Log to console in development
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.log(`Performance Metric - ${name}:`, value);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
// First Contentful Paint
|
||||
const fcpObserver = new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
const fcp = entries[entries.length - 1];
|
||||
if (fcp) {
|
||||
reportMetric("FCP", fcp.startTime);
|
||||
}
|
||||
});
|
||||
fcpObserver.observe({ entryTypes: ["paint"] });
|
||||
|
||||
// Largest Contentful Paint
|
||||
const lcpObserver = new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
const lcp = entries[entries.length - 1];
|
||||
if (lcp) {
|
||||
reportMetric("LCP", lcp.startTime);
|
||||
}
|
||||
});
|
||||
lcpObserver.observe({ entryTypes: ["largest-contentful-paint"] });
|
||||
|
||||
// First Input Delay
|
||||
const fidObserver = new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
entries.forEach((entry) => {
|
||||
const firstInputEntry = entry as PerformanceEventTiming;
|
||||
if (firstInputEntry.processingStart) {
|
||||
const fid = firstInputEntry.processingStart - firstInputEntry.startTime;
|
||||
reportMetric("FID", fid);
|
||||
}
|
||||
});
|
||||
});
|
||||
fidObserver.observe({ entryTypes: ["first-input"] });
|
||||
|
||||
// Cumulative Layout Shift
|
||||
let clsValue = 0;
|
||||
const clsObserver = new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
entries.forEach((entry: any) => {
|
||||
if (!entry.hadRecentInput) {
|
||||
clsValue += entry.value;
|
||||
}
|
||||
});
|
||||
});
|
||||
clsObserver.observe({ entryTypes: ["layout-shift"] });
|
||||
|
||||
// Time to First Byte
|
||||
const navigationEntry = performance.getEntriesByType("navigation")[0] as PerformanceNavigationTiming;
|
||||
if (navigationEntry) {
|
||||
const ttfb = navigationEntry.responseStart - navigationEntry.requestStart;
|
||||
reportMetric("TTFB", ttfb);
|
||||
}
|
||||
|
||||
// Report CLS on page unload
|
||||
const handleBeforeUnload = () => {
|
||||
if (clsValue > 0) {
|
||||
reportMetric("CLS", clsValue);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||
|
||||
return () => {
|
||||
fcpObserver.disconnect();
|
||||
lcpObserver.disconnect();
|
||||
fidObserver.disconnect();
|
||||
clsObserver.disconnect();
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
};
|
||||
}, [reportMetric]);
|
||||
|
||||
return {
|
||||
reportMetric,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,624 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { useRouter } from "@/components/navigation";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
RegistrationFormData,
|
||||
JournalistRegistrationData,
|
||||
PersonnelRegistrationData,
|
||||
GeneralRegistrationData,
|
||||
InstituteData,
|
||||
UserCategory,
|
||||
PasswordValidation,
|
||||
TimerState,
|
||||
Association,
|
||||
OTPRequestResponse,
|
||||
OTPVerificationResponse,
|
||||
LocationResponse,
|
||||
InstituteResponse,
|
||||
RegistrationResponse,
|
||||
LocationData,
|
||||
} from "@/types/registration";
|
||||
import {
|
||||
requestOTP,
|
||||
verifyRegistrationOTP,
|
||||
listProvince,
|
||||
listCity,
|
||||
listDistricts,
|
||||
listInstitusi,
|
||||
saveInstitutes,
|
||||
postRegistration,
|
||||
getDataJournalist,
|
||||
getDataPersonil,
|
||||
verifyOTP,
|
||||
} from "@/service/auth";
|
||||
import {
|
||||
validatePassword,
|
||||
validateUsername,
|
||||
validateEmail,
|
||||
validatePhoneNumber,
|
||||
createTimer,
|
||||
formatTime,
|
||||
sanitizeRegistrationData,
|
||||
sanitizeInstituteData,
|
||||
isValidCategory,
|
||||
getCategoryRoleId,
|
||||
showRegistrationError,
|
||||
showRegistrationSuccess,
|
||||
showRegistrationInfo,
|
||||
transformRegistrationData,
|
||||
createInitialFormData,
|
||||
validateIdentityData,
|
||||
RegistrationRateLimiter,
|
||||
REGISTRATION_CONSTANTS,
|
||||
} from "@/lib/registration-utils";
|
||||
|
||||
// Global rate limiter instance
|
||||
const registrationRateLimiter = new RegistrationRateLimiter();
|
||||
|
||||
// Hook for OTP operations
|
||||
export const useOTP = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [timer, setTimer] = useState<TimerState>(createTimer());
|
||||
|
||||
const startTimer = useCallback(() => {
|
||||
setTimer(createTimer());
|
||||
}, []);
|
||||
|
||||
const stopTimer = useCallback(() => {
|
||||
setTimer((prev) => ({
|
||||
...prev,
|
||||
isActive: false,
|
||||
isExpired: true,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Timer effect
|
||||
useEffect(() => {
|
||||
if (!timer.isActive || timer.countdown <= 0) {
|
||||
setTimer((prev) => ({
|
||||
...prev,
|
||||
isActive: false,
|
||||
isExpired: true,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setTimer((prev) => ({
|
||||
...prev,
|
||||
countdown: Math.max(0, prev.countdown - 1000),
|
||||
}));
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [timer.isActive, timer.countdown]);
|
||||
|
||||
const requestOTPCode = useCallback(
|
||||
async (
|
||||
email: string,
|
||||
category: UserCategory,
|
||||
memberIdentity?: string
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Check rate limiting
|
||||
const identifier = `${email}-${category}`;
|
||||
if (!registrationRateLimiter.canAttempt(identifier)) {
|
||||
const remainingAttempts =
|
||||
registrationRateLimiter.getRemainingAttempts(identifier);
|
||||
throw new Error(
|
||||
`Too many OTP requests. Please try again later. Remaining attempts: ${remainingAttempts}`
|
||||
);
|
||||
}
|
||||
|
||||
const data = {
|
||||
memberIdentity: memberIdentity || null,
|
||||
email,
|
||||
category: getCategoryRoleId(category),
|
||||
name: email.split("@")[0],
|
||||
};
|
||||
|
||||
// Debug logging
|
||||
console.log("OTP Request Data:", data);
|
||||
console.log("Category before conversion:", category);
|
||||
console.log("Category after conversion:", getCategoryRoleId(category));
|
||||
|
||||
const response = await requestOTP(data);
|
||||
|
||||
if (response?.error) {
|
||||
registrationRateLimiter.recordAttempt(identifier);
|
||||
throw new Error(response.message || "Failed to send OTP");
|
||||
}
|
||||
|
||||
// Start timer on successful OTP request
|
||||
startTimer();
|
||||
showRegistrationInfo("OTP sent successfully. Please check your email.");
|
||||
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
const errorMessage = error?.message || "Failed to send OTP";
|
||||
setError(errorMessage);
|
||||
showRegistrationError(error, "Failed to send OTP");
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[startTimer]
|
||||
);
|
||||
|
||||
const verifyOTPCode = useCallback(
|
||||
async (
|
||||
email: string,
|
||||
otp: string,
|
||||
category: UserCategory,
|
||||
memberIdentity?: string
|
||||
): Promise<any> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
if (otp.length !== 6) {
|
||||
throw new Error("OTP must be exactly 6 digits");
|
||||
}
|
||||
|
||||
const data = {
|
||||
memberIdentity: memberIdentity || null,
|
||||
email,
|
||||
otp,
|
||||
category: getCategoryRoleId(category),
|
||||
};
|
||||
|
||||
const response = await verifyOTP(data.email, data.otp);
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error(response.message || "OTP verification failed");
|
||||
}
|
||||
|
||||
stopTimer();
|
||||
showRegistrationSuccess("OTP verified successfully");
|
||||
|
||||
return response?.data?.userData;
|
||||
} catch (error: any) {
|
||||
const errorMessage = error?.message || "OTP verification failed";
|
||||
setError(errorMessage);
|
||||
showRegistrationError(error, "OTP verification failed");
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[stopTimer]
|
||||
);
|
||||
|
||||
const resendOTP = useCallback(
|
||||
async (
|
||||
email: string,
|
||||
category: UserCategory,
|
||||
memberIdentity?: string
|
||||
): Promise<boolean> => {
|
||||
return await requestOTPCode(email, category, memberIdentity);
|
||||
},
|
||||
[requestOTPCode]
|
||||
);
|
||||
|
||||
return {
|
||||
requestOTP: requestOTPCode,
|
||||
verifyOTP: verifyOTPCode,
|
||||
resendOTP,
|
||||
loading,
|
||||
error,
|
||||
timer,
|
||||
formattedTime: formatTime(timer.countdown),
|
||||
canResend: timer.isExpired,
|
||||
};
|
||||
};
|
||||
|
||||
// Hook for location data
|
||||
export const useLocationData = () => {
|
||||
const [provinces, setProvinces] = useState<LocationData[]>([]);
|
||||
const [cities, setCities] = useState<LocationData[]>([]);
|
||||
const [districts, setDistricts] = useState<LocationData[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchProvinces = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await listProvince();
|
||||
|
||||
if (!response || response.error) {
|
||||
throw new Error(response?.message || "Failed to fetch provinces");
|
||||
}
|
||||
|
||||
setProvinces(response?.data?.data || []);
|
||||
} catch (error: any) {
|
||||
const errorMessage = error?.message || "Failed to fetch provinces";
|
||||
setError(errorMessage);
|
||||
showRegistrationError(error, "Failed to fetch provinces");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchCities = useCallback(async (provinceId: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await listCity(provinceId);
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error(response.message || "Failed to fetch cities");
|
||||
}
|
||||
|
||||
setCities(response?.data?.data || []);
|
||||
setDistricts([]); // Reset districts when province changes
|
||||
} catch (error: any) {
|
||||
const errorMessage = error?.message || "Failed to fetch cities";
|
||||
setError(errorMessage);
|
||||
showRegistrationError(error, "Failed to fetch cities");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchDistricts = useCallback(async (cityId: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await listDistricts(cityId);
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error(response.message || "Failed to fetch districts");
|
||||
}
|
||||
|
||||
setDistricts(response?.data?.data || []);
|
||||
} catch (error: any) {
|
||||
const errorMessage = error?.message || "Failed to fetch districts";
|
||||
setError(errorMessage);
|
||||
showRegistrationError(error, "Failed to fetch districts");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Load provinces on mount
|
||||
useEffect(() => {
|
||||
fetchProvinces();
|
||||
}, [fetchProvinces]);
|
||||
|
||||
return {
|
||||
provinces,
|
||||
cities,
|
||||
districts,
|
||||
loading,
|
||||
error,
|
||||
fetchProvinces,
|
||||
fetchCities,
|
||||
fetchDistricts,
|
||||
};
|
||||
};
|
||||
|
||||
// Hook for institute data
|
||||
export const useInstituteData = (category?: number) => {
|
||||
const [institutes, setInstitutes] = useState<InstituteData[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchInstitutes = useCallback(
|
||||
async (categoryId?: number) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await listInstitusi(categoryId || category);
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error(response.message || "Failed to fetch institutes");
|
||||
}
|
||||
|
||||
setInstitutes(response?.data?.data || []);
|
||||
} catch (error: any) {
|
||||
const errorMessage = error?.message || "Failed to fetch institutes";
|
||||
setError(errorMessage);
|
||||
showRegistrationError(error, "Failed to fetch institutes");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[category]
|
||||
);
|
||||
|
||||
const saveInstitute = useCallback(
|
||||
async (instituteData: InstituteData): Promise<number> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const sanitizedData = sanitizeInstituteData(instituteData);
|
||||
|
||||
const response = await saveInstitutes({
|
||||
name: sanitizedData.name,
|
||||
address: sanitizedData.address,
|
||||
categoryRoleId: category || 6, // Use provided category or default to Journalist category
|
||||
});
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error(response.message || "Failed to save institute");
|
||||
}
|
||||
|
||||
return response?.data?.data?.id || 1;
|
||||
} catch (error: any) {
|
||||
const errorMessage = error?.message || "Failed to save institute";
|
||||
setError(errorMessage);
|
||||
showRegistrationError(error, "Failed to save institute");
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[category]
|
||||
);
|
||||
|
||||
// Load institutes on mount if category is provided
|
||||
useEffect(() => {
|
||||
if (category) {
|
||||
fetchInstitutes();
|
||||
}
|
||||
}, [fetchInstitutes, category]);
|
||||
|
||||
return {
|
||||
institutes,
|
||||
loading,
|
||||
error,
|
||||
fetchInstitutes,
|
||||
saveInstitute,
|
||||
};
|
||||
};
|
||||
|
||||
// Hook for user data validation
|
||||
export const useUserDataValidation = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const validateJournalistData = useCallback(
|
||||
async (certificateNumber: string): Promise<any> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await getDataJournalist(certificateNumber);
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error(
|
||||
response.message || "Invalid journalist certificate number"
|
||||
);
|
||||
}
|
||||
|
||||
return response?.data?.data;
|
||||
} catch (error: any) {
|
||||
const errorMessage =
|
||||
error?.message || "Failed to validate journalist data";
|
||||
setError(errorMessage);
|
||||
showRegistrationError(error, "Failed to validate journalist data");
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const validatePersonnelData = useCallback(
|
||||
async (policeNumber: string): Promise<any> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await getDataPersonil(policeNumber);
|
||||
|
||||
if (response?.error) {
|
||||
throw new Error(response.message || "Invalid police number");
|
||||
}
|
||||
|
||||
return response?.data?.data;
|
||||
} catch (error: any) {
|
||||
const errorMessage =
|
||||
error?.message || "Failed to validate personnel data";
|
||||
setError(errorMessage);
|
||||
showRegistrationError(error, "Failed to validate personnel data");
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
validateJournalistData,
|
||||
validatePersonnelData,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
// Hook for registration submission
|
||||
export const useRegistration = () => {
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const submitRegistration = useCallback(
|
||||
async (
|
||||
data: RegistrationFormData,
|
||||
category: UserCategory,
|
||||
userData: any,
|
||||
instituteId?: number
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Sanitize and validate data
|
||||
const sanitizedData = sanitizeRegistrationData(data);
|
||||
|
||||
// Validate password
|
||||
const passwordValidation = validatePassword(
|
||||
sanitizedData.password,
|
||||
sanitizedData.passwordConf
|
||||
);
|
||||
if (!passwordValidation.isValid) {
|
||||
throw new Error(passwordValidation.errors[0]);
|
||||
}
|
||||
|
||||
// Validate username
|
||||
const usernameValidation = validateUsername(sanitizedData.username);
|
||||
if (!usernameValidation.isValid) {
|
||||
throw new Error(usernameValidation.error!);
|
||||
}
|
||||
|
||||
// Transform data for API
|
||||
const transformedData = transformRegistrationData(
|
||||
sanitizedData,
|
||||
category,
|
||||
userData,
|
||||
instituteId
|
||||
);
|
||||
|
||||
const response = await postRegistration(transformedData);
|
||||
console.log("PPPP", transformedData);
|
||||
if (response?.error) {
|
||||
throw new Error(response.message || "Registration failed");
|
||||
}
|
||||
|
||||
showRegistrationSuccess(
|
||||
"Registration successful! Please check your email for verification."
|
||||
);
|
||||
|
||||
// Redirect to login page
|
||||
setTimeout(() => {
|
||||
router.push("/auth");
|
||||
}, 2000);
|
||||
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
const errorMessage = error?.message || "Registration failed";
|
||||
setError(errorMessage);
|
||||
showRegistrationError(error, "Registration failed");
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
return {
|
||||
submitRegistration,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
// Hook for form validation
|
||||
export const useFormValidation = () => {
|
||||
const validateIdentityForm = useCallback(
|
||||
(
|
||||
data:
|
||||
| JournalistRegistrationData
|
||||
| PersonnelRegistrationData
|
||||
| GeneralRegistrationData,
|
||||
category: UserCategory
|
||||
): { isValid: boolean; errors: string[] } => {
|
||||
return validateIdentityData(data, category);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const validateProfileForm = useCallback(
|
||||
(data: RegistrationFormData): { isValid: boolean; errors: string[] } => {
|
||||
const errors: string[] = [];
|
||||
|
||||
// Validate required fields
|
||||
if (!data.firstName?.trim()) {
|
||||
errors.push("Full name is required");
|
||||
}
|
||||
|
||||
if (!data.username?.trim()) {
|
||||
errors.push("Username is required");
|
||||
} else {
|
||||
const usernameValidation = validateUsername(data.username);
|
||||
if (!usernameValidation.isValid) {
|
||||
errors.push(usernameValidation.error!);
|
||||
}
|
||||
}
|
||||
|
||||
if (!data.email?.trim()) {
|
||||
errors.push("Email is required");
|
||||
} else {
|
||||
const emailValidation = validateEmail(data.email);
|
||||
if (!emailValidation.isValid) {
|
||||
errors.push(emailValidation.error!);
|
||||
}
|
||||
}
|
||||
|
||||
if (!data.phoneNumber?.trim()) {
|
||||
errors.push("Phone number is required");
|
||||
} else {
|
||||
const phoneValidation = validatePhoneNumber(data.phoneNumber);
|
||||
if (!phoneValidation.isValid) {
|
||||
errors.push(phoneValidation.error!);
|
||||
}
|
||||
}
|
||||
|
||||
if (!data.address?.trim()) {
|
||||
errors.push("Address is required");
|
||||
}
|
||||
|
||||
if (!data.provinsi) {
|
||||
errors.push("Province is required");
|
||||
}
|
||||
|
||||
if (!data.kota) {
|
||||
errors.push("City is required");
|
||||
}
|
||||
|
||||
if (!data.kecamatan) {
|
||||
errors.push("Subdistrict is required");
|
||||
}
|
||||
|
||||
if (!data.password) {
|
||||
errors.push("Password is required");
|
||||
} else {
|
||||
const passwordValidation = validatePassword(
|
||||
data.password,
|
||||
data.passwordConf
|
||||
);
|
||||
if (!passwordValidation.isValid) {
|
||||
errors.push(passwordValidation.errors[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
validateIdentityForm,
|
||||
validateProfileForm,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import { useAtom } from "jotai"
|
||||
import { atomWithStorage } from "jotai/utils"
|
||||
|
||||
export type TodoConfig = {
|
||||
isOpen: boolean
|
||||
}
|
||||
|
||||
const todoConfigAtom = atomWithStorage<TodoConfig>("todoConfig", {
|
||||
isOpen: false,
|
||||
|
||||
})
|
||||
|
||||
export function useTodoConfig() {
|
||||
|
||||
return useAtom(todoConfigAtom)
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import { useTranslations } from "next-intl";
|
||||
|
||||
/**
|
||||
* Custom hook that provides translation function with default values
|
||||
* This ensures that when translations are not ready, a default value is shown
|
||||
* Format: t("key", { defaultValue: "Default Value" })
|
||||
*/
|
||||
export function useTranslationWithDefaults(namespace: string) {
|
||||
const t = useTranslations(namespace);
|
||||
|
||||
return (key: string, options?: { defaultValue?: string }) => {
|
||||
try {
|
||||
const translation = t(key);
|
||||
// If translation returns the same key (fallback behavior), use default value
|
||||
if (translation === key && options?.defaultValue) {
|
||||
return options.defaultValue;
|
||||
}
|
||||
return translation || options?.defaultValue || key;
|
||||
} catch (error) {
|
||||
// If translation fails (e.g., key not found), return default value or key
|
||||
return options?.defaultValue || key;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
export const getYAxisConfig = (colors: any) => ({
|
||||
labels: getLabel(colors),
|
||||
});
|
||||
|
||||
|
||||
|
||||
export const getXAxisConfig = (colors: string): { } => ({
|
||||
categories: [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
],
|
||||
labels: getLabel(colors),
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
},
|
||||
});
|
||||
|
||||
export const getLabel = (colors:any): { } => ({
|
||||
style: {
|
||||
colors: colors,
|
||||
fontFamily: "Inter",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
export const getGridConfig = (): { show: boolean; borderColor: string; strokeDashArray: number; position: string; } => ({
|
||||
show: false,
|
||||
borderColor: 'transparent',
|
||||
strokeDashArray: 10,
|
||||
position: "back",
|
||||
});
|
||||
|
|
@ -1,246 +0,0 @@
|
|||
import Cookies from "js-cookie";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
AuthCookies,
|
||||
ProfileData,
|
||||
LoginFormData,
|
||||
NavigationConfig
|
||||
} from "@/types/auth";
|
||||
import { getCookiesDecrypt, setCookiesEncrypt } from "@/lib/utils";
|
||||
|
||||
// Navigation configuration based on user roles
|
||||
export const NAVIGATION_CONFIG: NavigationConfig[] = [
|
||||
{ roleId: 2, path: "/in/dashboard/executive", label: "Executive Dashboard" },
|
||||
{ roleId: 3, path: "/in/dashboard", label: "Dashboard" },
|
||||
{ roleId: 4, path: "/in/dashboard", label: "Dashboard" },
|
||||
{ roleId: 9, path: "/in/supervisor/ticketing/all", label: "Ticketing" },
|
||||
{ roleId: 10, path: "/in/dashboard", label: "Dashboard" },
|
||||
{ roleId: 11, path: "/in/dashboard", label: "Dashboard" },
|
||||
{ roleId: 12, path: "/in/dashboard", label: "Dashboard" },
|
||||
{ roleId: 13, path: "/in/dashboard", label: "Dashboard" },
|
||||
{ roleId: 14, path: "/in/dashboard", label: "Dashboard" },
|
||||
{ roleId: 15, path: "/in/dashboard", label: "Dashboard" },
|
||||
{ roleId: 18, path: "/in/dashboard/executive-data", label: "Executive Data Dashboard" },
|
||||
{ roleId: 19, path: "/in/dashboard", label: "Dashboard" },
|
||||
];
|
||||
|
||||
// Cookie management utilities
|
||||
export const setAuthCookies = (accessToken: string, refreshToken: string): void => {
|
||||
const dateTime = new Date();
|
||||
const newTime = dateTime.getTime() + 10 * 60 * 1000; // 10 minutes
|
||||
|
||||
Cookies.set("access_token", accessToken, { expires: 1 });
|
||||
Cookies.set("refresh_token", refreshToken, { expires: 1 });
|
||||
Cookies.set("time_refresh", new Date(newTime).toISOString(), { expires: 1 });
|
||||
Cookies.set("is_first_login", String(true), { secure: true, sameSite: "strict" });
|
||||
};
|
||||
|
||||
export const setProfileCookies = (profile: ProfileData): void => {
|
||||
Cookies.set("home_path", profile.homePath || "", { expires: 1 });
|
||||
Cookies.set("profile_picture", profile.profilePictureUrl || "", { expires: 1 });
|
||||
Cookies.set("state", profile.userLevel?.name || "", { expires: 1 });
|
||||
Cookies.set("state-prov", profile.userLevel?.province?.provName || "", { expires: 1 });
|
||||
|
||||
setCookiesEncrypt("uie", profile.id, { expires: 1 });
|
||||
setCookiesEncrypt("urie", profile.roleId.toString(), { expires: 1 });
|
||||
setCookiesEncrypt("urne", profile.role?.name || "", { expires: 1 });
|
||||
setCookiesEncrypt("ulie", profile.userLevel?.id.toString() || "", { expires: 1 });
|
||||
setCookiesEncrypt("uplie", profile.userLevel?.parentLevelId?.toString() || "", { expires: 1 });
|
||||
setCookiesEncrypt("ulne", profile.userLevel?.levelNumber.toString() || "", { expires: 1 });
|
||||
setCookiesEncrypt("ufne", profile.fullname, { expires: 1 });
|
||||
setCookiesEncrypt("ulnae", profile.userLevel?.name || "", { expires: 1 });
|
||||
setCookiesEncrypt("uinse", profile.instituteId || "", { expires: 1 });
|
||||
|
||||
Cookies.set("status", "login", { expires: 1 });
|
||||
};
|
||||
|
||||
export const clearAllCookies = (): void => {
|
||||
Object.keys(Cookies.get()).forEach((cookieName) => {
|
||||
Cookies.remove(cookieName);
|
||||
});
|
||||
};
|
||||
|
||||
export const getAuthCookies = (): Partial<AuthCookies> => {
|
||||
return {
|
||||
access_token: Cookies.get("access_token"),
|
||||
refresh_token: Cookies.get("refresh_token"),
|
||||
time_refresh: Cookies.get("time_refresh"),
|
||||
is_first_login: Cookies.get("is_first_login"),
|
||||
home_path: Cookies.get("home_path"),
|
||||
profile_picture: Cookies.get("profile_picture"),
|
||||
state: Cookies.get("state"),
|
||||
"state-prov": Cookies.get("state-prov"),
|
||||
uie: getCookiesDecrypt("uie"),
|
||||
urie: getCookiesDecrypt("urie"),
|
||||
urne: getCookiesDecrypt("urne"),
|
||||
ulie: getCookiesDecrypt("ulie"),
|
||||
uplie: getCookiesDecrypt("uplie"),
|
||||
ulne: getCookiesDecrypt("ulne"),
|
||||
ufne: getCookiesDecrypt("ufne"),
|
||||
ulnae: getCookiesDecrypt("ulnae"),
|
||||
uinse: getCookiesDecrypt("uinse"),
|
||||
status: Cookies.get("status"),
|
||||
};
|
||||
};
|
||||
|
||||
// User validation utilities
|
||||
export const isUserEligible = (profile: ProfileData): boolean => {
|
||||
return !(
|
||||
profile.isInternational ||
|
||||
!profile.isActive ||
|
||||
profile.isDelete
|
||||
);
|
||||
};
|
||||
|
||||
export const isProtectedRole = (roleId: number): boolean => {
|
||||
const protectedRoles = [2, 3, 4, 9, 10, 11, 12, 18, 19];
|
||||
return protectedRoles.includes(roleId);
|
||||
};
|
||||
|
||||
export const isSpecialLevel = (userLevelId: number, parentLevelId?: number): boolean => {
|
||||
return userLevelId === 794 || parentLevelId === 761;
|
||||
};
|
||||
|
||||
// Navigation utilities
|
||||
export const getNavigationPath = (roleId: number, userLevelId?: number, parentLevelId?: number): string => {
|
||||
const config = NAVIGATION_CONFIG.find(nav => nav.roleId === roleId);
|
||||
|
||||
console.log("Navigation Config : ", config);
|
||||
|
||||
if (config) {
|
||||
// Special handling for role 2 with specific level conditions
|
||||
if (roleId === 2 && userLevelId && parentLevelId && isSpecialLevel(userLevelId, parentLevelId)) {
|
||||
return "/in/dashboard";
|
||||
}
|
||||
return config.path;
|
||||
}
|
||||
|
||||
return "/"; // Default fallback
|
||||
};
|
||||
|
||||
// Error handling utilities
|
||||
export const handleAuthError = (error: any, defaultMessage: string = "Authentication failed"): string => {
|
||||
if (typeof error === "string") {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (error?.message) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
if (error?.response?.data?.message) {
|
||||
return error.response.data.message;
|
||||
}
|
||||
|
||||
return defaultMessage;
|
||||
};
|
||||
|
||||
export const showAuthError = (error: any, defaultMessage: string = "Authentication failed"): void => {
|
||||
const message = handleAuthError(error, defaultMessage);
|
||||
toast.error(message);
|
||||
};
|
||||
|
||||
export const showAuthSuccess = (message: string): void => {
|
||||
toast.success(message);
|
||||
};
|
||||
|
||||
// Form validation utilities
|
||||
export const validateEmail = (email: string): boolean => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
};
|
||||
|
||||
export const validatePassword = (password: string): { isValid: boolean; errors: string[] } => {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (password.length < 6) {
|
||||
errors.push("Password must be at least 6 characters long");
|
||||
}
|
||||
|
||||
if (password.length > 100) {
|
||||
errors.push("Password must be less than 100 characters");
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
};
|
||||
|
||||
// Loading state utilities
|
||||
export const createLoadingState = () => {
|
||||
let isLoading = false;
|
||||
|
||||
return {
|
||||
get: () => isLoading,
|
||||
set: (loading: boolean) => { isLoading = loading; },
|
||||
withLoading: async <T>(fn: () => Promise<T>): Promise<T> => {
|
||||
isLoading = true;
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Constants
|
||||
export const AUTH_CONSTANTS = {
|
||||
CLIENT_ID: "mediahub-app",
|
||||
GRANT_TYPE: "password",
|
||||
TOKEN_REFRESH_INTERVAL: 10 * 60 * 1000, // 10 minutes
|
||||
MAX_LOGIN_ATTEMPTS: 3,
|
||||
LOCKOUT_DURATION: 15 * 60 * 1000, // 15 minutes
|
||||
} as const;
|
||||
|
||||
// Rate limiting utilities
|
||||
export class LoginRateLimiter {
|
||||
private attempts: Map<string, { count: number; lastAttempt: number }> = new Map();
|
||||
|
||||
canAttempt(username: string): boolean {
|
||||
const userAttempts = this.attempts.get(username);
|
||||
|
||||
if (!userAttempts) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const timeSinceLastAttempt = Date.now() - userAttempts.lastAttempt;
|
||||
|
||||
if (timeSinceLastAttempt > AUTH_CONSTANTS.LOCKOUT_DURATION) {
|
||||
this.attempts.delete(username);
|
||||
return true;
|
||||
}
|
||||
|
||||
return userAttempts.count < AUTH_CONSTANTS.MAX_LOGIN_ATTEMPTS;
|
||||
}
|
||||
|
||||
recordAttempt(username: string): void {
|
||||
const userAttempts = this.attempts.get(username);
|
||||
|
||||
if (userAttempts) {
|
||||
userAttempts.count++;
|
||||
userAttempts.lastAttempt = Date.now();
|
||||
} else {
|
||||
this.attempts.set(username, { count: 1, lastAttempt: Date.now() });
|
||||
}
|
||||
}
|
||||
|
||||
resetAttempts(username: string): void {
|
||||
this.attempts.delete(username);
|
||||
}
|
||||
|
||||
getRemainingTime(username: string): number {
|
||||
const userAttempts = this.attempts.get(username);
|
||||
|
||||
if (!userAttempts) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const timeSinceLastAttempt = Date.now() - userAttempts.lastAttempt;
|
||||
return Math.max(0, AUTH_CONSTANTS.LOCKOUT_DURATION - timeSinceLastAttempt);
|
||||
}
|
||||
}
|
||||
|
||||
// Global rate limiter instance
|
||||
export const loginRateLimiter = new LoginRateLimiter();
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
// import NextAuth from "next-auth";
|
||||
// import GitHub from "next-auth/providers/github";
|
||||
// import Google from "next-auth/providers/google";
|
||||
|
||||
// import CredentialsProvider from "next-auth/providers/credentials";
|
||||
// import { getUserByEmail, type User } from "./data";
|
||||
|
||||
|
||||
|
||||
// export const { auth, handlers, signIn, signOut } = NextAuth({
|
||||
|
||||
// session: {
|
||||
// strategy: "jwt",
|
||||
// },
|
||||
// providers: [
|
||||
// Google,
|
||||
// GitHub,
|
||||
// CredentialsProvider({
|
||||
// credentials: {
|
||||
|
||||
// email: {},
|
||||
// password: {},
|
||||
// },
|
||||
// async authorize(credentials) {
|
||||
// if (credentials === null) return null;
|
||||
|
||||
// try {
|
||||
// const user = getUserByEmail(credentials?.email as string);
|
||||
// if (user) {
|
||||
// const isMatch = user?.password === credentials.password;
|
||||
|
||||
// if (isMatch) {
|
||||
// return user;
|
||||
// } else {
|
||||
// throw new Error("Email or Password is not correct");
|
||||
// }
|
||||
// } else {
|
||||
// throw new Error("User not found");
|
||||
// }
|
||||
// } catch (error) {
|
||||
// throw new Error(error as string);
|
||||
// }
|
||||
// },
|
||||
// }),
|
||||
|
||||
// ],
|
||||
// });
|
||||
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
export const colors = {
|
||||
primary: "#4669FA",
|
||||
secondary: "#A0AEC0",
|
||||
danger: "#F1595C",
|
||||
warning: "#FA916B",
|
||||
info: "#0CE7FA",
|
||||
light: "#425466",
|
||||
success: "#50C793",
|
||||
default: "#0f172a",
|
||||
'default-800': "#1e293b",
|
||||
'default-600': "#475569",
|
||||
'default-300': "#cbd5e1"
|
||||
|
||||
};
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
// user data
|
||||
const users = [
|
||||
{
|
||||
name: "dashcode",
|
||||
email: "dashcode@codeshaper.net",
|
||||
password: "password",
|
||||
image: '/images/users/user-1.jpg',
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
export type User = (typeof users)[number]
|
||||
|
||||
export const getUserByEmail = (email: string) => {
|
||||
return users.find((user) => user.email === email)
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import va from "@vercel/analytics"
|
||||
import { z } from "zod"
|
||||
|
||||
const eventSchema = z.object({
|
||||
name: z.enum([
|
||||
"copy_npm_command",
|
||||
"copy_usage_import_code",
|
||||
"copy_usage_code",
|
||||
"copy_primitive_code",
|
||||
"copy_theme_code",
|
||||
"copy_block_code",
|
||||
"copy_chunk_code",
|
||||
"enable_lift_mode",
|
||||
"copy_chart_code",
|
||||
"copy_chart_theme",
|
||||
"copy_chart_data",
|
||||
"copy_color",
|
||||
]),
|
||||
properties: z
|
||||
.record(z.union([z.string(), z.number(), z.boolean(), z.null()]))
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export type Event = z.infer<typeof eventSchema>
|
||||
|
||||
export function trackEvent(input: Event): void {
|
||||
const event = eventSchema.parse(input)
|
||||
if (event) {
|
||||
va.track(event.name, event.properties)
|
||||
}
|
||||
}
|
||||
5589
lib/lib/menus.ts
5589
lib/lib/menus.ts
File diff suppressed because it is too large
Load Diff
|
|
@ -1,374 +0,0 @@
|
|||
import {
|
||||
RegistrationFormData,
|
||||
JournalistRegistrationData,
|
||||
PersonnelRegistrationData,
|
||||
GeneralRegistrationData,
|
||||
InstituteData,
|
||||
UserCategory,
|
||||
PasswordValidation,
|
||||
TimerState,
|
||||
Association
|
||||
} from "@/types/registration";
|
||||
import { toast } from "sonner";
|
||||
|
||||
// Constants
|
||||
export const REGISTRATION_CONSTANTS = {
|
||||
OTP_TIMEOUT: 60000, // 1 minute in milliseconds
|
||||
MAX_OTP_ATTEMPTS: 3,
|
||||
PASSWORD_MIN_LENGTH: 8,
|
||||
USERNAME_MIN_LENGTH: 3,
|
||||
USERNAME_MAX_LENGTH: 50,
|
||||
} as const;
|
||||
|
||||
// Association data
|
||||
export const ASSOCIATIONS: Association[] = [
|
||||
{ id: "1", name: "PWI (Persatuan Wartawan Indonesia)", value: "PWI" },
|
||||
{ id: "2", name: "IJTI (Ikatan Jurnalis Televisi Indonesia)", value: "IJTI" },
|
||||
{ id: "3", name: "PFI (Pewarta Foto Indonesia)", value: "PFI" },
|
||||
{ id: "4", name: "AJI (Asosiasi Jurnalis Indonesia)", value: "AJI" },
|
||||
{ id: "5", name: "Other Identity", value: "Wartawan" },
|
||||
];
|
||||
|
||||
// Password validation utility
|
||||
export const validatePassword = (password: string, confirmPassword?: string): PasswordValidation => {
|
||||
const errors: string[] = [];
|
||||
let strength: 'weak' | 'medium' | 'strong' = 'weak';
|
||||
|
||||
// Check minimum length
|
||||
if (password.length < REGISTRATION_CONSTANTS.PASSWORD_MIN_LENGTH) {
|
||||
errors.push(`Password must be at least ${REGISTRATION_CONSTANTS.PASSWORD_MIN_LENGTH} characters`);
|
||||
}
|
||||
|
||||
// Check for uppercase letter
|
||||
if (!/[A-Z]/.test(password)) {
|
||||
errors.push("Password must contain at least one uppercase letter");
|
||||
}
|
||||
|
||||
// Check for lowercase letter
|
||||
if (!/[a-z]/.test(password)) {
|
||||
errors.push("Password must contain at least one lowercase letter");
|
||||
}
|
||||
|
||||
// Check for number
|
||||
if (!/\d/.test(password)) {
|
||||
errors.push("Password must contain at least one number");
|
||||
}
|
||||
|
||||
// Check for special character
|
||||
if (!/[@$!%*?&]/.test(password)) {
|
||||
errors.push("Password must contain at least one special character (@$!%*?&)");
|
||||
}
|
||||
|
||||
// Check password confirmation
|
||||
if (confirmPassword && password !== confirmPassword) {
|
||||
errors.push("Passwords don't match");
|
||||
}
|
||||
|
||||
// Determine strength
|
||||
if (password.length >= 12 && errors.length === 0) {
|
||||
strength = 'strong';
|
||||
} else if (password.length >= 8 && errors.length <= 1) {
|
||||
strength = 'medium';
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
strength,
|
||||
};
|
||||
};
|
||||
|
||||
// Username validation utility
|
||||
export const validateUsername = (username: string): { isValid: boolean; error?: string } => {
|
||||
if (username.length < REGISTRATION_CONSTANTS.USERNAME_MIN_LENGTH) {
|
||||
return { isValid: false, error: `Username must be at least ${REGISTRATION_CONSTANTS.USERNAME_MIN_LENGTH} characters` };
|
||||
}
|
||||
|
||||
if (username.length > REGISTRATION_CONSTANTS.USERNAME_MAX_LENGTH) {
|
||||
return { isValid: false, error: `Username must be less than ${REGISTRATION_CONSTANTS.USERNAME_MAX_LENGTH} characters` };
|
||||
}
|
||||
|
||||
if (!/^[a-zA-Z0-9._-]+$/.test(username)) {
|
||||
return { isValid: false, error: "Username can only contain letters, numbers, dots, underscores, and hyphens" };
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
};
|
||||
|
||||
// Email validation utility
|
||||
export const validateEmail = (email: string): { isValid: boolean; error?: string } => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
if (!email) {
|
||||
return { isValid: false, error: "Email is required" };
|
||||
}
|
||||
|
||||
if (!emailRegex.test(email)) {
|
||||
return { isValid: false, error: "Please enter a valid email address" };
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
};
|
||||
|
||||
// Phone number validation utility
|
||||
export const validatePhoneNumber = (phoneNumber: string): { isValid: boolean; error?: string } => {
|
||||
const phoneRegex = /^[0-9+\-\s()]+$/;
|
||||
|
||||
if (!phoneNumber) {
|
||||
return { isValid: false, error: "Phone number is required" };
|
||||
}
|
||||
|
||||
if (!phoneRegex.test(phoneNumber)) {
|
||||
return { isValid: false, error: "Please enter a valid phone number" };
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
};
|
||||
|
||||
// Timer utility
|
||||
export const createTimer = (duration: number = REGISTRATION_CONSTANTS.OTP_TIMEOUT): TimerState => {
|
||||
return {
|
||||
countdown: duration,
|
||||
isActive: true,
|
||||
isExpired: false,
|
||||
};
|
||||
};
|
||||
|
||||
export const formatTime = (milliseconds: number): string => {
|
||||
const minutes = Math.floor(milliseconds / 60000);
|
||||
const seconds = Math.floor((milliseconds % 60000) / 1000);
|
||||
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
// Data sanitization utility
|
||||
export const sanitizeRegistrationData = (data: RegistrationFormData): RegistrationFormData => {
|
||||
return {
|
||||
...data,
|
||||
firstName: data.firstName.trim(),
|
||||
username: data.username.trim().toLowerCase(),
|
||||
email: data.email.trim().toLowerCase(),
|
||||
phoneNumber: data.phoneNumber.trim(),
|
||||
address: data.address.trim(),
|
||||
};
|
||||
};
|
||||
|
||||
export const sanitizeInstituteData = (data: InstituteData): InstituteData => {
|
||||
return {
|
||||
id : data.id,
|
||||
name: data.name.trim(),
|
||||
address: data.address.trim(),
|
||||
};
|
||||
};
|
||||
|
||||
// Category validation utility
|
||||
export const isValidCategory = (category: string): category is UserCategory => {
|
||||
return category === "6" || category === "7" || category === "general";
|
||||
};
|
||||
|
||||
export const getCategoryLabel = (category: UserCategory): string => {
|
||||
switch (category) {
|
||||
case "6":
|
||||
return "Journalist";
|
||||
case "7":
|
||||
return "Personnel";
|
||||
case "general":
|
||||
return "Public";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
};
|
||||
|
||||
// Category to role ID conversion utility
|
||||
export const getCategoryRoleId = (category: UserCategory): number => {
|
||||
switch (category) {
|
||||
case "6":
|
||||
return 6; // Journalist
|
||||
case "7":
|
||||
return 7; // Personnel
|
||||
case "general":
|
||||
return 5; // Public (general)
|
||||
default:
|
||||
return 5; // Default to public
|
||||
}
|
||||
};
|
||||
|
||||
// Error handling utilities
|
||||
export const showRegistrationError = (error: any, defaultMessage: string = "Registration failed") => {
|
||||
const message = error?.message || error?.data?.message || defaultMessage;
|
||||
toast.error(message);
|
||||
console.error("Registration error:", error);
|
||||
};
|
||||
|
||||
export const showRegistrationSuccess = (message: string = "Registration successful") => {
|
||||
toast.success(message);
|
||||
};
|
||||
|
||||
export const showRegistrationInfo = (message: string) => {
|
||||
toast.info(message);
|
||||
};
|
||||
|
||||
// Data transformation utilities
|
||||
export const transformRegistrationData = (
|
||||
data: RegistrationFormData,
|
||||
category: UserCategory,
|
||||
userData: any,
|
||||
instituteId?: number
|
||||
): any => {
|
||||
const baseData = {
|
||||
firstName: data.firstName,
|
||||
lastName: data.firstName, // Using firstName as lastName for now
|
||||
username: data.username,
|
||||
phoneNumber: data.phoneNumber,
|
||||
email: data.email,
|
||||
address: data.address,
|
||||
provinceId: Number(data.provinsi),
|
||||
cityId: Number(data.kota),
|
||||
districtId: Number(data.kecamatan),
|
||||
password: data.password,
|
||||
roleId: getCategoryRoleId(category),
|
||||
};
|
||||
|
||||
// Add category-specific data
|
||||
if (category === "6") {
|
||||
return {
|
||||
...baseData,
|
||||
memberIdentity: userData?.journalistCertificate,
|
||||
instituteId: instituteId || 1,
|
||||
};
|
||||
} else if (category === "7") {
|
||||
return {
|
||||
...baseData,
|
||||
memberIdentity: userData?.policeNumber,
|
||||
instituteId: 1,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...baseData,
|
||||
memberIdentity: null,
|
||||
instituteId: 1,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Form data utilities
|
||||
export const createInitialFormData = (category: UserCategory) => {
|
||||
const baseData = {
|
||||
firstName: "",
|
||||
username: "",
|
||||
phoneNumber: "",
|
||||
email: "",
|
||||
address: "",
|
||||
provinsi: "",
|
||||
kota: "",
|
||||
kecamatan: "",
|
||||
password: "",
|
||||
passwordConf: "",
|
||||
};
|
||||
|
||||
if (category === "6") {
|
||||
return {
|
||||
...baseData,
|
||||
journalistCertificate: "",
|
||||
association: "",
|
||||
};
|
||||
} else if (category === "7") {
|
||||
return {
|
||||
...baseData,
|
||||
policeNumber: "",
|
||||
};
|
||||
}
|
||||
|
||||
return baseData;
|
||||
};
|
||||
|
||||
// Validation utilities
|
||||
export const validateIdentityData = (
|
||||
data: JournalistRegistrationData | PersonnelRegistrationData | GeneralRegistrationData,
|
||||
category: UserCategory
|
||||
): { isValid: boolean; errors: string[] } => {
|
||||
const errors: string[] = [];
|
||||
|
||||
// Email validation
|
||||
const emailValidation = validateEmail(data.email);
|
||||
if (!emailValidation.isValid) {
|
||||
errors.push(emailValidation.error!);
|
||||
}
|
||||
|
||||
// Category-specific validation
|
||||
if (category === "6") {
|
||||
const journalistData = data as JournalistRegistrationData;
|
||||
if (!journalistData.journalistCertificate?.trim()) {
|
||||
errors.push("Journalist certificate number is required");
|
||||
}
|
||||
if (!journalistData.association?.trim()) {
|
||||
errors.push("Association is required");
|
||||
}
|
||||
} else if (category === "7") {
|
||||
const personnelData = data as PersonnelRegistrationData;
|
||||
if (!personnelData.policeNumber?.trim()) {
|
||||
errors.push("Police number is required");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
};
|
||||
|
||||
// Rate limiting utility
|
||||
export class RegistrationRateLimiter {
|
||||
private attempts: Map<string, { count: number; lastAttempt: number }> = new Map();
|
||||
private readonly maxAttempts: number;
|
||||
private readonly windowMs: number;
|
||||
|
||||
constructor(maxAttempts: number = REGISTRATION_CONSTANTS.MAX_OTP_ATTEMPTS, windowMs: number = 300000) {
|
||||
this.maxAttempts = maxAttempts;
|
||||
this.windowMs = windowMs;
|
||||
}
|
||||
|
||||
canAttempt(identifier: string): boolean {
|
||||
const attempt = this.attempts.get(identifier);
|
||||
if (!attempt) return true;
|
||||
|
||||
const now = Date.now();
|
||||
if (now - attempt.lastAttempt > this.windowMs) {
|
||||
this.attempts.delete(identifier);
|
||||
return true;
|
||||
}
|
||||
|
||||
return attempt.count < this.maxAttempts;
|
||||
}
|
||||
|
||||
recordAttempt(identifier: string): void {
|
||||
const attempt = this.attempts.get(identifier);
|
||||
const now = Date.now();
|
||||
|
||||
if (attempt) {
|
||||
attempt.count++;
|
||||
attempt.lastAttempt = now;
|
||||
} else {
|
||||
this.attempts.set(identifier, { count: 1, lastAttempt: now });
|
||||
}
|
||||
}
|
||||
|
||||
getRemainingAttempts(identifier: string): number {
|
||||
const attempt = this.attempts.get(identifier);
|
||||
if (!attempt) return this.maxAttempts;
|
||||
|
||||
const now = Date.now();
|
||||
if (now - attempt.lastAttempt > this.windowMs) {
|
||||
this.attempts.delete(identifier);
|
||||
return this.maxAttempts;
|
||||
}
|
||||
|
||||
return Math.max(0, this.maxAttempts - attempt.count);
|
||||
}
|
||||
|
||||
reset(identifier: string): void {
|
||||
this.attempts.delete(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
// Export rate limiter instance
|
||||
export const registrationRateLimiter = new RegistrationRateLimiter();
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
export const generateStaticUUIDs = (prefix: string, count: number): string[] => {
|
||||
const uuids = [];
|
||||
for (let i = 1; i <= count; i++) {
|
||||
uuids.push(`${prefix}-${i.toString().padStart(3, '0')}`);
|
||||
}
|
||||
return uuids;
|
||||
};
|
||||
|
||||
export const generateStaticNumbers = (min: number, max: number, count: number): number[] => {
|
||||
const numbers = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
// Generate deterministic but varied numbers
|
||||
const seed = i * 7 + 13; // Simple seed for variety
|
||||
const range = max - min;
|
||||
const value = min + (seed % range);
|
||||
numbers.push(value);
|
||||
}
|
||||
return numbers;
|
||||
};
|
||||
|
||||
export const generateStaticChartData = (labels: string[], datasets: number): number[][] => {
|
||||
const result = [];
|
||||
for (let i = 0; i < datasets; i++) {
|
||||
const data = generateStaticNumbers(-100, 100, labels.length);
|
||||
result.push(data);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// Pre-generated static UUIDs for common use cases
|
||||
export const STATIC_UUIDS = {
|
||||
PRODUCTS: generateStaticUUIDs('product', 20),
|
||||
EVENTS: generateStaticUUIDs('event', 20),
|
||||
TASKS: generateStaticUUIDs('task', 20),
|
||||
PROJECTS: generateStaticUUIDs('project', 20),
|
||||
USERS: generateStaticUUIDs('user', 20),
|
||||
COLUMNS: generateStaticUUIDs('col', 10),
|
||||
};
|
||||
|
||||
// Pre-generated chart data
|
||||
export const CHART_DATA = {
|
||||
LINE_CHART_3_DATASETS: [
|
||||
[45, -23, 67, -12, 89, -34, 56],
|
||||
[-67, 34, -89, 12, -45, 78, -23],
|
||||
[23, -56, 78, -34, 45, -67, 89]
|
||||
],
|
||||
LINE_CHART_2_DATASETS: [
|
||||
[25, 45, 67, 34, 89, 56, 78],
|
||||
[67, 23, 45, 78, 34, 89, 12]
|
||||
],
|
||||
POSITIVE_DATA: [25, 45, 67, 34, 89, 56, 78, 90, 45, 67],
|
||||
MIXED_DATA: [45, -23, 67, -12, 89, -34, 56, 78, -45, 23],
|
||||
TIME_SERIES: [12, 34, 56, 78, 90, 45, 67, 89, 23, 45, 67, 89]
|
||||
};
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
import Swal from "sweetalert2";
|
||||
import withReactContent from "sweetalert2-react-content";
|
||||
|
||||
const MySwal = withReactContent(Swal);
|
||||
|
||||
const Toast = MySwal.mixin({
|
||||
toast: true,
|
||||
position: "top-end",
|
||||
showConfirmButton: false,
|
||||
timer: 3000,
|
||||
timerProgressBar: true,
|
||||
didOpen: (toast) => {
|
||||
toast.addEventListener("mouseenter", Swal.stopTimer);
|
||||
toast.addEventListener("mouseleave", Swal.resumeTimer);
|
||||
},
|
||||
});
|
||||
|
||||
export function loading(msg?: any) {
|
||||
let timerInterval: any;
|
||||
MySwal.fire({
|
||||
title: msg || "Loading...",
|
||||
allowOutsideClick: false,
|
||||
timerProgressBar: true,
|
||||
didOpen: () => {
|
||||
MySwal.showLoading();
|
||||
timerInterval = setInterval(() => {}, 100);
|
||||
},
|
||||
willClose: () => {
|
||||
clearInterval(timerInterval);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function error(msg?: any) {
|
||||
MySwal.fire({
|
||||
icon: "error",
|
||||
title: "Failed...",
|
||||
text: msg || "Unknown Error",
|
||||
});
|
||||
}
|
||||
|
||||
export function successRouter(redirect: string, router?: any) {
|
||||
MySwal.fire({
|
||||
title: "Success!",
|
||||
icon: "success",
|
||||
confirmButtonColor: "#3085d6",
|
||||
confirmButtonText: "Ok",
|
||||
allowOutsideClick: false,
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
router.push(redirect);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function success(title: string) {
|
||||
MySwal.fire({
|
||||
title: title || "Success!",
|
||||
icon: "success",
|
||||
confirmButtonColor: "#3085d6",
|
||||
confirmButtonText: "OK",
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function close() {
|
||||
MySwal.close();
|
||||
}
|
||||
|
||||
export function warning(text: string, redirect: string, router?: any) {
|
||||
MySwal.fire({
|
||||
title: text,
|
||||
icon: "warning",
|
||||
confirmButtonColor: "#3085d6",
|
||||
confirmButtonText: "OK",
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
router.push(redirect);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function successToast(title: string, text: string) {
|
||||
Toast.fire({
|
||||
icon: "error",
|
||||
title: title,
|
||||
text: text,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
export type color = "default" | "primary" | "secondary" | "success" | "info" | "warning" | "destructive"
|
||||
export type InputColor = "default" | "primary" | "secondary" | "success" | "info" | "warning" | "destructive"
|
||||
export type shadow = "sm" | "md" | "lg" | "xl"
|
||||
export type size = "default" | "sm" | "md" | "lg"
|
||||
export type rounded = "sm" | "md" | "lg" | "full"
|
||||
export type radius = "sm" | "md" | "lg" | "xl" | "none"
|
||||
|
||||
|
||||
// config
|
||||
export type layoutType = "vertical" | "horizontal" | "semi-box" | "compact";
|
||||
export type contentType = "wide" | "boxed";
|
||||
export type skinType = "default" | "bordered";
|
||||
export type sidebarType = 'classic' | 'draggable' | 'two-column' | 'compact'
|
||||
export type navBarType = 'floating' | 'sticky' | 'hidden' | 'default'
|
||||
export type headerColorType = 'default' | 'coloured' | 'transparent'
|
||||
106
lib/lib/utils.ts
106
lib/lib/utils.ts
|
|
@ -1,106 +0,0 @@
|
|||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import Cookies from "js-cookie";
|
||||
import CryptoJS from "crypto-js";
|
||||
import Swal from "sweetalert2";
|
||||
import withReactContent from "sweetalert2-react-content";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
const MySwal = withReactContent(Swal);
|
||||
|
||||
const Toast = MySwal.mixin({
|
||||
toast: true,
|
||||
position: "top-end",
|
||||
showConfirmButton: false,
|
||||
timer: 3000,
|
||||
timerProgressBar: true,
|
||||
didOpen: (toast) => {
|
||||
toast.addEventListener("mouseenter", Swal.stopTimer);
|
||||
toast.addEventListener("mouseleave", Swal.resumeTimer);
|
||||
},
|
||||
});
|
||||
|
||||
export const hexToRGB = (hex: any, alpha?: number): any => {
|
||||
const r = parseInt(hex.slice(1, 3), 16);
|
||||
const g = parseInt(hex.slice(3, 5), 16);
|
||||
const b = parseInt(hex.slice(5, 7), 16);
|
||||
|
||||
if (alpha) {
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
} else {
|
||||
return `rgb(${r}, ${g}, ${b})`;
|
||||
}
|
||||
};
|
||||
|
||||
export function getCookiesDecrypt(param: any) {
|
||||
const cookiesEncrypt = Cookies.get(param);
|
||||
try {
|
||||
if (cookiesEncrypt != undefined) {
|
||||
const output = CryptoJS.AES.decrypt(
|
||||
cookiesEncrypt.toString(),
|
||||
`${param}_EncryptKey@mediahub`
|
||||
).toString(CryptoJS.enc.Utf8);
|
||||
if (output.startsWith('"')) {
|
||||
return output.slice(1, -1);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Error", cookiesEncrypt);
|
||||
}
|
||||
}
|
||||
|
||||
export function successToast(title: string, text: string) {
|
||||
Toast.fire({
|
||||
icon: "error",
|
||||
title: title,
|
||||
text: text,
|
||||
});
|
||||
}
|
||||
|
||||
export function setCookiesEncrypt(
|
||||
param: string,
|
||||
data: any,
|
||||
options?: Cookies.CookieAttributes
|
||||
) {
|
||||
// Enkripsi data
|
||||
const cookiesEncrypt = CryptoJS.AES.encrypt(
|
||||
JSON.stringify(data),
|
||||
`${param}_EncryptKey@mediahub`
|
||||
).toString(); // Tambahkan .toString() di sini
|
||||
|
||||
// Simpan data terenkripsi di cookie
|
||||
Cookies.set(param, cookiesEncrypt, options);
|
||||
}
|
||||
|
||||
export function checkAuthorization(page: any) {
|
||||
const roleId = getCookiesDecrypt("urie");
|
||||
const levelNumber = getCookiesDecrypt("ulne");
|
||||
if (
|
||||
(Number(roleId) !== 3 &&
|
||||
Number(roleId) !== 4 &&
|
||||
Number(roleId) !== 11 &&
|
||||
Number(roleId) !== 12 &&
|
||||
Number(roleId) !== 2) ||
|
||||
roleId == undefined
|
||||
) {
|
||||
console.log("Wrong Authentication");
|
||||
// window.location.href = "/";
|
||||
} else if (page == "admin" && Number(levelNumber) !== 1) {
|
||||
console.log("Wrong Authentication Admin");
|
||||
// window.location.href = "/";
|
||||
}
|
||||
}
|
||||
|
||||
export function checkLoginSession() {
|
||||
const userId = getCookiesDecrypt("uie");
|
||||
const jwt = Cookies.get("access_token");
|
||||
const data = {
|
||||
userId,
|
||||
jwt,
|
||||
};
|
||||
// doCheckSession(data);
|
||||
}
|
||||
307
lib/menus.ts
307
lib/menus.ts
|
|
@ -36,6 +36,7 @@ export function getMenuList(pathname: string, t: any): Group[] {
|
|||
const userLevelId = getCookiesDecrypt("ulie");
|
||||
const userParentLevelId = getCookiesDecrypt("uplie");
|
||||
const hideForRole14 = roleId === "14";
|
||||
const userRoleId: number = Number(getCookiesDecrypt("urie"));
|
||||
|
||||
let menusSelected = <any>[];
|
||||
|
||||
|
|
@ -56,7 +57,7 @@ export function getMenuList(pathname: string, t: any): Group[] {
|
|||
},
|
||||
],
|
||||
},
|
||||
...(Number(roleId) === 3
|
||||
...(Number(userRoleId) === 3
|
||||
? [
|
||||
{
|
||||
groupLabel: "",
|
||||
|
|
@ -151,45 +152,45 @@ export function getMenuList(pathname: string, t: any): Group[] {
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupLabel: "",
|
||||
id: "settings",
|
||||
menus: [
|
||||
{
|
||||
id: "settings",
|
||||
href: "/admin/settings",
|
||||
label: t("settings"),
|
||||
active: pathname.includes("/settinng"),
|
||||
icon: "material-symbols:settings",
|
||||
submenus: [
|
||||
{
|
||||
href: "/admin/categories",
|
||||
label: t("category"),
|
||||
active: pathname === "/admin/settings/category",
|
||||
icon: "heroicons:arrow-trending-up",
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
href: "/admin/settings/tag",
|
||||
label: "Tag",
|
||||
active: pathname === "/admin/settings/tag",
|
||||
icon: "heroicons:arrow-trending-up",
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
href: "/admin/settings/banner",
|
||||
label: "Banner",
|
||||
active: pathname === "/admin/settings/banner",
|
||||
icon: "heroicons:arrow-trending-up",
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
// {
|
||||
// groupLabel: "",
|
||||
// id: "settings",
|
||||
// menus: [
|
||||
// {
|
||||
// id: "settings",
|
||||
// href: "/admin/settings",
|
||||
// label: t("settings"),
|
||||
// active: pathname.includes("/settinng"),
|
||||
// icon: "material-symbols:settings",
|
||||
// submenus: [
|
||||
// {
|
||||
// href: "/admin/categories",
|
||||
// label: t("category"),
|
||||
// active: pathname === "/admin/settings/category",
|
||||
// icon: "heroicons:arrow-trending-up",
|
||||
// children: [],
|
||||
// },
|
||||
// // {
|
||||
// // href: "/admin/settings/tag",
|
||||
// // label: "Tag",
|
||||
// // active: pathname === "/admin/settings/tag",
|
||||
// // icon: "heroicons:arrow-trending-up",
|
||||
// // children: [],
|
||||
// // },
|
||||
// // {
|
||||
// // href: "/admin/settings/banner",
|
||||
// // label: "Banner",
|
||||
// // active: pathname === "/admin/settings/banner",
|
||||
// // icon: "heroicons:arrow-trending-up",
|
||||
// // children: [],
|
||||
// // },
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
]
|
||||
: []),
|
||||
...(Number(roleId) === 2
|
||||
...(Number(userRoleId) === 2
|
||||
? [
|
||||
{
|
||||
groupLabel: "",
|
||||
|
|
@ -219,68 +220,104 @@ export function getMenuList(pathname: string, t: any): Group[] {
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupLabel: "",
|
||||
id: "settings",
|
||||
menus: [
|
||||
{
|
||||
id: "settings",
|
||||
href: "/admin/settings",
|
||||
label: t("settings"),
|
||||
active: pathname.includes("/settinng"),
|
||||
icon: "material-symbols:settings",
|
||||
submenus: [
|
||||
{
|
||||
href: "/admin/categories",
|
||||
label: t("category"),
|
||||
active: pathname === "/admin/settings/category",
|
||||
icon: "heroicons:arrow-trending-up",
|
||||
children: [],
|
||||
},
|
||||
// {
|
||||
// href: "/admin/settings/tag",
|
||||
// label: "Tag",
|
||||
// active: pathname === "/admin/settings/tag",
|
||||
// icon: "heroicons:arrow-trending-up",
|
||||
// children: [],
|
||||
// },
|
||||
// {
|
||||
// href: "/admin/settings/banner",
|
||||
// label: "Banner",
|
||||
// active: pathname === "/admin/settings/banner",
|
||||
// icon: "heroicons:arrow-trending-up",
|
||||
// children: [],
|
||||
// },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(Number(roleId) === 1
|
||||
? [
|
||||
{
|
||||
groupLabel: "",
|
||||
id: "management-user",
|
||||
menus: [
|
||||
{
|
||||
id: "management-user-menu",
|
||||
href: "/admin/management-user",
|
||||
label: "Management User",
|
||||
active: pathname.includes("/management-user"),
|
||||
icon: "clarity:users-solid",
|
||||
submenus: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupLabel: "",
|
||||
id: "tenant",
|
||||
menus: [
|
||||
{
|
||||
id: "tenant",
|
||||
href: "/admin/tenants",
|
||||
label: "Tenant",
|
||||
active: pathname.includes("/tenant"),
|
||||
icon: "material-symbols:domain",
|
||||
submenus: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupLabel: "",
|
||||
id: "menu-management",
|
||||
menus: [
|
||||
{
|
||||
id: "menu-management",
|
||||
href: "/admin/settings/menu-management",
|
||||
label: "Menu Management",
|
||||
active: pathname === "/admin/settings/menu-management",
|
||||
icon: "heroicons:bars-3",
|
||||
submenus: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
// {
|
||||
// groupLabel: "",
|
||||
// id: "module-management",
|
||||
// menus: [
|
||||
// {
|
||||
// id: "module-management",
|
||||
// href: "/admin/settings/module-management",
|
||||
// label: "Module Management",
|
||||
// active: pathname === "/admin/settings/module-management",
|
||||
// icon: "heroicons:puzzle-piece",
|
||||
// submenus: [],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
]
|
||||
: []),
|
||||
? [
|
||||
{
|
||||
groupLabel: "",
|
||||
id: "management-user",
|
||||
menus: [
|
||||
{
|
||||
id: "management-user-menu",
|
||||
href: "/admin/management-user",
|
||||
label: "Management User",
|
||||
active: pathname.includes("/management-user"),
|
||||
icon: "clarity:users-solid",
|
||||
submenus: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupLabel: "",
|
||||
id: "tenant",
|
||||
menus: [
|
||||
{
|
||||
id: "tenant",
|
||||
href: "/admin/tenants",
|
||||
label: "Tenant",
|
||||
active: pathname.includes("/tenant"),
|
||||
icon: "material-symbols:domain",
|
||||
submenus: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupLabel: "",
|
||||
id: "menu-management",
|
||||
menus: [
|
||||
{
|
||||
id: "menu-management",
|
||||
href: "/admin/settings/menu-management",
|
||||
label: "Menu Management",
|
||||
active: pathname === "/admin/settings/menu-management",
|
||||
icon: "heroicons:bars-3",
|
||||
submenus: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
// {
|
||||
// groupLabel: "",
|
||||
// id: "module-management",
|
||||
// menus: [
|
||||
// {
|
||||
// id: "module-management",
|
||||
// href: "/admin/settings/module-management",
|
||||
// label: "Module Management",
|
||||
// active: pathname === "/admin/settings/module-management",
|
||||
// icon: "heroicons:puzzle-piece",
|
||||
// submenus: [],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
]
|
||||
: []),
|
||||
];
|
||||
return menusSelected;
|
||||
}
|
||||
|
|
@ -5470,7 +5507,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/appex-charts/charts-appex-area",
|
||||
label: t("areaCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/appex-charts/charts-appex-area"
|
||||
"/charts/appex-charts/charts-appex-area",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5478,7 +5515,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/appex-charts/charts-appex-bar",
|
||||
label: t("barCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/appex-charts/charts-appex-bar"
|
||||
"/charts/appex-charts/charts-appex-bar",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5486,7 +5523,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/appex-charts/charts-appex-boxplot",
|
||||
label: t("boxplotCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/appex-charts/charts-appex-boxplot"
|
||||
"/charts/appex-charts/charts-appex-boxplot",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5494,7 +5531,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/appex-charts/charts-appex-bubble",
|
||||
label: t("bubbleCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/appex-charts/charts-appex-bubble"
|
||||
"/charts/appex-charts/charts-appex-bubble",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5502,7 +5539,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/appex-charts/charts-appex-candlestick",
|
||||
label: t("candlestickCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/appex-charts/charts-appex-candlestick"
|
||||
"/charts/appex-charts/charts-appex-candlestick",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5510,7 +5547,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/appex-charts/charts-appex-column",
|
||||
label: t("columnCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/appex-charts/charts-appex-column"
|
||||
"/charts/appex-charts/charts-appex-column",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5518,7 +5555,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/appex-charts/charts-appex-combo",
|
||||
label: t("comboCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/appex-charts/charts-appex-combo"
|
||||
"/charts/appex-charts/charts-appex-combo",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5527,7 +5564,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/appex-charts/charts-appex-funnel",
|
||||
label: t("funnelCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/appex-charts/charts-appex-funnel"
|
||||
"/charts/appex-charts/charts-appex-funnel",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5535,7 +5572,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/appex-charts/charts-appex-heatmap",
|
||||
label: t("heatmapCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/appex-charts/charts-appex-heatmap"
|
||||
"/charts/appex-charts/charts-appex-heatmap",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5543,7 +5580,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/appex-charts/charts-appex-line",
|
||||
label: t("lineCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/appex-charts/charts-appex-line"
|
||||
"/charts/appex-charts/charts-appex-line",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5551,7 +5588,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/appex-charts/charts-appex-pie",
|
||||
label: t("pieCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/appex-charts/charts-appex-pie"
|
||||
"/charts/appex-charts/charts-appex-pie",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5559,7 +5596,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/appex-charts/charts-appex-polararea",
|
||||
label: t("ploarareaCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/appex-charts/charts-appex-polararea"
|
||||
"/charts/appex-charts/charts-appex-polararea",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5567,7 +5604,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/appex-charts/charts-appex-radar",
|
||||
label: t("radarCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/appex-charts/charts-appex-radar"
|
||||
"/charts/appex-charts/charts-appex-radar",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5575,7 +5612,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/appex-charts/charts-appex-radialbars",
|
||||
label: t("radialbarCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/appex-charts/charts-appex-radialbars"
|
||||
"/charts/appex-charts/charts-appex-radialbars",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5583,7 +5620,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/appex-charts/charts-appex-range",
|
||||
label: t("rangeCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/appex-charts/charts-appex-range"
|
||||
"/charts/appex-charts/charts-appex-range",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5591,7 +5628,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/appex-charts/charts-appex-scatter",
|
||||
label: t("scatterCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/appex-charts/charts-appex-scatter"
|
||||
"/charts/appex-charts/charts-appex-scatter",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5599,7 +5636,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/appex-charts/charts-appex-timeline",
|
||||
label: t("timelineCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/appex-charts/charts-appex-timeline"
|
||||
"/charts/appex-charts/charts-appex-timeline",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5607,7 +5644,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/appex-charts/charts-appex-treemap",
|
||||
label: t("treemapCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/appex-charts/charts-appex-treemap"
|
||||
"/charts/appex-charts/charts-appex-treemap",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5623,7 +5660,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/rechart/charts-rechart-area",
|
||||
label: t("areaCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/rechart/charts-rechart-area"
|
||||
"/charts/rechart/charts-rechart-area",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5631,7 +5668,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/rechart/charts-rechart-bar",
|
||||
label: t("barCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/rechart/charts-rechart-bar"
|
||||
"/charts/rechart/charts-rechart-bar",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5639,7 +5676,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/rechart/charts-rechart-composed",
|
||||
label: t("composedCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/rechart/charts-rechart-composed"
|
||||
"/charts/rechart/charts-rechart-composed",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5647,7 +5684,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/rechart/charts-rechart-line",
|
||||
label: t("lineCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/rechart/charts-rechart-line"
|
||||
"/charts/rechart/charts-rechart-line",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5655,7 +5692,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/rechart/charts-rechart-pie",
|
||||
label: t("pieCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/rechart/charts-rechart-pie"
|
||||
"/charts/rechart/charts-rechart-pie",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5663,7 +5700,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/rechart/charts-rechart-radar",
|
||||
label: t("radarCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/rechart/charts-rechart-radar"
|
||||
"/charts/rechart/charts-rechart-radar",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5671,7 +5708,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/rechart/charts-rechart-radialbar",
|
||||
label: t("radialbarCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/rechart/charts-rechart-radialbar"
|
||||
"/charts/rechart/charts-rechart-radialbar",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5679,7 +5716,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/rechart/charts-rechart-scatter",
|
||||
label: t("scatterCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/rechart/charts-rechart-scatter"
|
||||
"/charts/rechart/charts-rechart-scatter",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5687,7 +5724,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/rechart/charts-rechart-treemap",
|
||||
label: t("treemapCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/rechart/charts-rechart-treemap"
|
||||
"/charts/rechart/charts-rechart-treemap",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5703,7 +5740,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/chart-js/charts-chartjs-area",
|
||||
label: t("areaCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/chart-js/charts-chartjs-area"
|
||||
"/charts/chart-js/charts-chartjs-area",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5711,7 +5748,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/chart-js/charts-chartjs-bar",
|
||||
label: t("barCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/chart-js/charts-chartjs-bar"
|
||||
"/charts/chart-js/charts-chartjs-bar",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5719,7 +5756,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/chart-js/charts-chartjs-line",
|
||||
label: t("lineCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/chart-js/charts-chartjs-line"
|
||||
"/charts/chart-js/charts-chartjs-line",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5727,7 +5764,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/chart-js/charts-chartjs-animations",
|
||||
label: t("animationCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/chart-js/charts-chartjs-animations"
|
||||
"/charts/chart-js/charts-chartjs-animations",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5735,7 +5772,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/chart-js/charts-chartjs-legend",
|
||||
label: t("legendCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/chart-js/charts-chartjs-legend"
|
||||
"/charts/chart-js/charts-chartjs-legend",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5743,7 +5780,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/chart-js/charts-chartjs-scaleoptions",
|
||||
label: t("scaleOptionCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/chart-js/charts-chartjs-scaleoptions"
|
||||
"/charts/chart-js/charts-chartjs-scaleoptions",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5751,7 +5788,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/chart-js/charts-chartjs-scales",
|
||||
label: t("scaleCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/chart-js/charts-chartjs-scales"
|
||||
"/charts/chart-js/charts-chartjs-scales",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5759,7 +5796,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/chart-js/charts-chartjs-scriptable",
|
||||
label: t("scriptableCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/chart-js/charts-chartjs-scriptable"
|
||||
"/charts/chart-js/charts-chartjs-scriptable",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5767,7 +5804,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/chart-js/charts-chartjs-title",
|
||||
label: t("titleCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/chart-js/charts-chartjs-title"
|
||||
"/charts/chart-js/charts-chartjs-title",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5775,7 +5812,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/chart-js/charts-chartjs-tooltip",
|
||||
label: t("tooltipChart"),
|
||||
active: pathname.includes(
|
||||
"/charts/chart-js/charts-chartjs-tooltip"
|
||||
"/charts/chart-js/charts-chartjs-tooltip",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -5783,7 +5820,7 @@ export function getHorizontalMenuList(pathname: string, t: any): Group[] {
|
|||
href: "/charts/chart-js/charts-chartjs-other",
|
||||
label: t("otherCharts"),
|
||||
active: pathname.includes(
|
||||
"/charts/chart-js/charts-chartjs-other"
|
||||
"/charts/chart-js/charts-chartjs-other",
|
||||
),
|
||||
children: [],
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue