Compare commits

..

No commits in common. "124b0cdfefaa5bb3165e9289cd60fde89c552cf1" and "283e8b4cdb27b053604151f5a956d1307a36d3eb" have entirely different histories.

17 changed files with 830 additions and 1126 deletions

View File

@ -29,11 +29,11 @@ import {
checkRolePlacementsAvailability,
getListCompetencies,
getListExperiences,
saveUserInternal,
saveUserRolePlacements,
} from "@/service/management-user/management-user";
import { error, loading } from "@/config/swal";
import { Eye, EyeOff } from "lucide-react";
import { saveUserInternal } from "@/service/service/management-user/management-user";
const FormSchema = z.object({
name: z.string({

View File

@ -29,12 +29,12 @@ import {
getListCompetencies,
getListExperiences,
getUserById,
saveUserInternal,
saveUserRolePlacements,
} from "@/service/management-user/management-user";
import { loading } from "@/config/swal";
import { Eye, EyeOff } from "lucide-react";
import { useParams } from "next/navigation";
import { saveUserInternal } from "@/service/service/management-user/management-user";
const FormSchema = z.object({
name: z.string({

View File

@ -1,5 +1,6 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Link, useRouter } from "@/i18n/routing";

View File

@ -208,7 +208,7 @@ function TenantSettingsContentTable() {
</p>
</div>
<div className="flex items-center gap-2">
{/* <SettingsIcon className="h-6 w-6 text-gray-500" /> */}
<SettingsIcon className="h-6 w-6 text-gray-500" />
{/* <Button variant="outline" size="sm" onClick={checkWorkflowStatus}>
Check Workflow Status
</Button> */}

View File

@ -50,8 +50,6 @@ export default function CategoriesDetailForm() {
if (id) {
try {
const res = await getArticleCategoryDetail(Number(id));
const user = await getUserInfo();
setDetail(res?.data?.data);
} catch (err) {
console.error("Error fetching category detail:", err);
@ -138,9 +136,9 @@ export default function CategoriesDetailForm() {
{/* Status */}
<div>
<Label>Status</Label>
<p className="text-sm text-green-500">
<p className="text-sm text-slate-600">
{/* {detail.isPublish ? "Published" : "Draft"} |{" "} */}
{detail.isActive ? "* Active" : "- Inactive"}
{detail.isActive ? "Active" : "Inactive"}
</p>
</div>

View File

@ -6,7 +6,6 @@ import {
ArticleCategory,
} from "@/service/categories/article-categories";
import { useTranslations } from "next-intl";
import { RevealR } from "../ui/RevealR";
export default function Category() {
const t = useTranslations("MediaUpdate");
@ -22,7 +21,7 @@ export default function Category() {
// Filter hanya kategori yang aktif dan published
const activeCategories = response.data.data.filter(
(category: ArticleCategory) =>
category.isActive && category.isPublish,
category.isActive && category.isPublish
);
setCategories(activeCategories);
}
@ -56,7 +55,6 @@ export default function Category() {
categories.length > 0 ? categories : fallbackCategories;
return (
<RevealR>
<section className="px-4 py-10">
<div className="max-w-[1350px] mx-auto bg-white dark:bg-default-50 dark:border dark:border-slate-50 rounded-xl shadow-md p-6">
<h2 className="text-xl font-semibold mb-5">
@ -95,7 +93,7 @@ export default function Category() {
onClick={() => {
// Navigate to category page or search by category
console.log(
`Category clicked: ${categoryTitle} (${categorySlug})`,
`Category clicked: ${categoryTitle} (${categorySlug})`
);
// TODO: Implement navigation to category page
}}
@ -108,6 +106,5 @@ export default function Category() {
)}
</div>
</section>
</RevealR>
);
}

View File

@ -13,7 +13,6 @@ import "swiper/css";
import "swiper/css/navigation";
import LocalSwitcher from "../partials/header/locale-switcher";
import { useTranslations } from "next-intl";
import { Reveal } from "./Reveal";
// Custom styles for Swiper
const swiperStyles = `
@ -101,7 +100,6 @@ export default function Footer() {
}, []);
return (
<Reveal>
<footer className="border-t bg-white dark:bg-default-50 text-center">
<style jsx>{swiperStyles}</style>
<div className="max-w-[1350px] mx-auto">
@ -275,6 +273,5 @@ export default function Footer() {
</div>
</div>
</footer>
</Reveal>
);
}

View File

@ -18,9 +18,6 @@ import "swiper/css/navigation";
import "swiper/css/pagination";
import ImageBlurry from "../ui/image-blurry";
import { useTranslations } from "next-intl";
import { Reveal } from "../ui/Reveal";
import { RevealL } from "../ui/RevealL";
import { RevealR } from "../ui/RevealR";
const images = ["/PPS.png", "/PPS2.jpeg", "/PPS3.jpg", "/PPS4.png"];
@ -44,7 +41,7 @@ export default function Header() {
undefined,
undefined,
"createdAt",
slug,
slug
);
let articlesData: any[] = [];
@ -59,14 +56,14 @@ export default function Header() {
"createdAt",
"",
"",
"",
""
);
articlesData = (fallbackResponse?.data?.data?.content || []).filter(
(item: any) => item.typeId === 1,
(item: any) => item.typeId === 1
);
} else {
articlesData = (response?.data?.data || []).filter(
(item: any) => item.typeId === 1,
(item: any) => item.typeId === 1
);
}
@ -109,14 +106,14 @@ export default function Header() {
const ids = new Set<number>(
(Array.isArray(bookmarks) ? bookmarks : [])
.map((b: any) => Number(b.articleId ?? b.id ?? b.article?.id))
.filter((x) => !isNaN(x)),
.filter((x) => !isNaN(x))
);
const merged = new Set([...localSet, ...ids]);
setBookmarkedIds(merged);
localStorage.setItem(
"bookmarkedIds",
JSON.stringify(Array.from(merged)),
JSON.stringify(Array.from(merged))
);
}
} catch (error) {
@ -131,14 +128,13 @@ export default function Header() {
if (bookmarkedIds.size > 0) {
localStorage.setItem(
"bookmarkedIds",
JSON.stringify(Array.from(bookmarkedIds)),
JSON.stringify(Array.from(bookmarkedIds))
);
}
}, [bookmarkedIds]);
return (
<section className="max-w-[1350px] mx-auto px-4">
<RevealR>
<div className="flex flex-col lg:flex-row gap-6 py-6">
{data.length > 0 && (
<Card
@ -150,6 +146,7 @@ export default function Header() {
}
/>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 w-full">
{data.slice(1, 5).map((item) => (
<Card
@ -163,7 +160,6 @@ export default function Header() {
))}
</div>
</div>
</RevealR>
<div className="relative w-full h-48 sm:h-64 md:h-80 lg:h-[460px] mt-4 rounded-xl overflow-hidden">
<Swiper
@ -222,26 +218,12 @@ function Card({
const [isBookmarked, setIsBookmarked] = useState(isInitiallyBookmarked);
const DEFAULT_IMAGE = "/assets/logo1.png";
const [imageSrc, setImageSrc] = useState(DEFAULT_IMAGE);
const [imageSrc, setImageSrc] = useState(
item?.smallThumbnailLink || DEFAULT_IMAGE
);
useEffect(() => {
const src = item?.smallThumbnailLink;
if (!src) {
setImageSrc(DEFAULT_IMAGE);
return;
}
const img = new window.Image();
img.src = src;
img.onload = () => {
setImageSrc(src);
};
img.onerror = () => {
setImageSrc(DEFAULT_IMAGE);
};
setImageSrc(item?.smallThumbnailLink || DEFAULT_IMAGE);
}, [item?.smallThumbnailLink]);
useEffect(() => {
@ -284,7 +266,7 @@ function Card({
newSet.add(Number(item.id));
localStorage.setItem(
"bookmarkedIds",
JSON.stringify(Array.from(newSet)),
JSON.stringify(Array.from(newSet))
);
MySwal.fire({
@ -310,7 +292,7 @@ function Card({
};
return (
// <RevealL>
<div>
<div
className={`rounded-xl overflow-hidden shadow hover:shadow-lg transition-all bg-white dark:bg-black dark:border dark:border-slate-50 ${
isBig
@ -330,10 +312,12 @@ function Card({
fill
className="object-cover"
/> */}
<ImageBlurry
<Image
src={imageSrc}
alt={item.title}
className="w-full h-full object-contain"
fill
className="object-cover"
onError={() => setImageSrc(DEFAULT_IMAGE)}
/>
</Link>
</div>
@ -395,7 +379,7 @@ function Card({
</div>
</div>
</div>
// </RevealL>
</div>
);
}

View File

@ -17,8 +17,6 @@ import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { ThumbsUp, ThumbsDown } from "lucide-react";
import { useTranslations } from "next-intl";
import ImageBlurry from "../ui/image-blurry";
import { RevealL } from "../ui/RevealL";
function formatTanggal(dateString: string) {
if (!dateString) return "";
@ -342,37 +340,24 @@ export default function MediaUpdate() {
};
function SafeImage({ src, alt }: { src?: string; alt?: string }) {
const [imgSrc, setImgSrc] = useState(DEFAULT_IMAGE);
const [imgSrc, setImgSrc] = useState(src || DEFAULT_IMAGE);
useEffect(() => {
if (!src) {
setImgSrc(DEFAULT_IMAGE);
return;
}
const img = new window.Image();
img.src = src;
img.onload = () => {
setImgSrc(src);
};
img.onerror = () => {
setImgSrc(DEFAULT_IMAGE);
};
setImgSrc(src || DEFAULT_IMAGE);
}, [src]);
return (
<ImageBlurry
<Image
src={imgSrc}
alt={alt || "Image"}
className="w-full h-full object-contain"
fill
className="object-cover cursor-pointer hover:opacity-90 transition-opacity"
onError={() => setImgSrc(DEFAULT_IMAGE)}
/>
);
}
return (
<RevealL>
<section className="bg-white dark:bg-default-50 px-4 py-10 border max-w-[1350px] mx-auto rounded-md border-[#CDD5DF] my-10">
<div className="max-w-screen-xl mx-auto">
<h2 className="text-2xl font-semibold text-center mb-6">
@ -595,6 +580,5 @@ export default function MediaUpdate() {
)}
</div>
</section>
</RevealL>
);
}

View File

@ -14,7 +14,6 @@ import { DynamicLogoTenant } from "./dynamic-logo-tenant";
import { useTranslations } from "next-intl";
import LocalSwitcher from "../partials/header/locale-switcher";
import ThemeSwitcher from "../partials/header/theme-switcher";
import { RevealT } from "../ui/RevealT";
export default function Navbar() {
const t = useTranslations("Navbar");
@ -76,7 +75,6 @@ export default function Navbar() {
const fullname = Cookies.get("ufne");
return (
<RevealT>
<header className="relative max-w-[1400px] mx-auto flex items-center justify-between px-4 py-3 border-b bg-white dark:bg-default-50 z-50">
<div className="flex flex-row items-center justify-between space-x-4 z-10">
<Menu
@ -342,31 +340,19 @@ export default function Navbar() {
</div>
<div className="space-y-5 text-[16px] font-bold">
<Link
href="/about"
className="block text-black dark:text-white"
>
<Link href="/about" className="block text-black dark:text-white">
{t("about")}
</Link>
<Link
href="/advertising"
className="block text-black dark:text-white"
>
<Link href="/advertising" className="block text-black dark:text-white">
{t("advertising")}
</Link>
<Link
href="/contact"
className="block text-black dark:text-white"
>
<Link href="/contact" className="block text-black dark:text-white">
{t("contact")}
</Link>
{!isLoggedIn ? (
<>
<Link
href="/auth"
className="block text-lg text-gray-800 dark:text-white"
>
<Link href="/auth" className="block text-lg text-gray-800 dark:text-white">
{t("login")}
</Link>
<Link
@ -404,6 +390,5 @@ export default function Navbar() {
</div>
)}
</header>
</RevealT>
);
}

View File

@ -93,17 +93,17 @@ export default function ImageDetail({ id }: { id: number }) {
files:
article.files?.map((f: any) => ({
id: f.id,
url: f.fileUrl,
fileName: f.fileName,
filePath: f.filePath,
fileThumbnail: f.fileThumbnail,
fileAlt: f.fileAlt,
widthPixel: f.widthPixel,
heightPixel: f.heightPixel,
url: f.file_url,
fileName: f.file_name,
filePath: f.file_path,
fileThumbnail: f.file_thumbnail,
fileAlt: f.file_alt,
widthPixel: f.width_pixel,
heightPixel: f.height_pixel,
size: f.size,
downloadCount: f.downloadCount,
createdAt: f.createdAt,
updatedAt: f.updatedAt,
downloadCount: f.download_count,
createdAt: f.created_at,
updatedAt: f.updated_at,
})) || [],
};
@ -155,7 +155,7 @@ export default function ImageDetail({ id }: { id: number }) {
<div className="relative">
<Image
placeholder={`data:image/svg+xml;base64,${toBase64(
shimmer(700, 475),
shimmer(700, 475)
)}`}
width={2560}
height={1440}
@ -185,7 +185,7 @@ export default function ImageDetail({ id }: { id: number }) {
<a onClick={() => setSelectedImage(index)} key={file?.id}>
<Image
placeholder={`data:image/svg+xml;base64,${toBase64(
shimmer(700, 475),
shimmer(700, 475)
)}`}
width={1920}
height={1080}
@ -223,7 +223,7 @@ export default function ImageDetail({ id }: { id: number }) {
</span>
<span className="flex items-center gap-1 border-r-2 pr-2 border-black text-black">
<Eye className="w-4 h-4" />
{data.clickCount || 0}{" "}
{data.viewCount || 0}
</span>
<span className="text-black">
Creator: {data.creatorGroupLevelName}

View File

@ -1,58 +0,0 @@
import React, { useRef, useEffect } from "react";
import { motion, useInView, useAnimation } from "framer-motion";
interface Props {
children: React.ReactNode;
}
export const Reveal = ({ children }: Props) => {
const ref = useRef(null);
const isInView = useInView(ref, { once: false });
const mainControls = useAnimation();
const slideControls = useAnimation();
useEffect(() => {
if (isInView) {
mainControls.start("visible");
slideControls.start("visible");
} else mainControls.start("hidden");
}, [isInView]);
return (
<div ref={ref}>
<motion.div
variants={{
hidden: { opacity: 0, y: 75 },
visible: { opacity: 1, y: 0 },
}}
initial="hidden"
animate={mainControls}
transition={{
duration: 1,
delay: 0.1,
}}
>
{children}
</motion.div>
{/* TODO green slide thingy */}
{/* <motion.div
variants={{
hidden: { left: 0 },
visible: { left: "100%" },
}}
initial="hidden"
animate={slideControls}
transition={{ duration: 0.5, ease: "easeIn" }}
style={{
position: "absolute",
top: 4,
bottom: 4,
left: 0,
right: 0,
background: "#5e84ff",
zIndex: 20,
}}
/> */}
</div>
);
};

View File

@ -1,63 +0,0 @@
import React, { useRef, useEffect } from "react";
import { motion, useInView, useAnimation } from "framer-motion";
interface Props {
children: React.ReactNode;
}
export const RevealL = ({ children }: Props) => {
const ref = useRef(null);
const isInView = useInView(ref, { once: false });
const mainControls = useAnimation();
const slideControls = useAnimation();
useEffect(() => {
if (isInView) {
mainControls.start("visible");
slideControls.start("visible");
} else {
mainControls.start("hidden");
}
}, [isInView]);
return (
<div ref={ref} className="relative overflow-hidden">
<motion.div
variants={{
hidden: { opacity: 0, x: -75 }, // ← muncul dari kiri
visible: { opacity: 1, x: 0 },
}}
initial="hidden"
animate={mainControls}
transition={{
duration: 1,
delay: 0.1,
}}
>
{children}
</motion.div>
{/* Optional: Slide Overlay Animation */}
{/*
<motion.div
variants={{
hidden: { left: 0 },
visible: { left: "100%" },
}}
initial="hidden"
animate={slideControls}
transition={{ duration: 0.5, ease: "easeIn" }}
style={{
position: "absolute",
top: 4,
bottom: 4,
left: 0,
right: 0,
background: "#5e84ff",
zIndex: 20,
}}
/>
*/}
</div>
);
};

View File

@ -1,63 +0,0 @@
import React, { useRef, useEffect } from "react";
import { motion, useInView, useAnimation } from "framer-motion";
interface Props {
children: React.ReactNode;
}
export const RevealR = ({ children }: Props) => {
const ref = useRef(null);
const isInView = useInView(ref, { once: false });
const mainControls = useAnimation();
const slideControls = useAnimation();
useEffect(() => {
if (isInView) {
mainControls.start("visible");
slideControls.start("visible");
} else {
mainControls.start("hidden");
}
}, [isInView]);
return (
<div ref={ref} className="relative overflow-hidden">
<motion.div
variants={{
hidden: { opacity: 0, x: 75 },
visible: { opacity: 1, x: 0 },
}}
initial="hidden"
animate={mainControls}
transition={{
duration: 1,
delay: 0.1,
}}
>
{children}
</motion.div>
{/* Optional: Slide Overlay Animation */}
{/*
<motion.div
variants={{
hidden: { left: 0 },
visible: { left: "100%" },
}}
initial="hidden"
animate={slideControls}
transition={{ duration: 0.5, ease: "easeIn" }}
style={{
position: "absolute",
top: 4,
bottom: 4,
left: 0,
right: 0,
background: "#5e84ff",
zIndex: 20,
}}
/>
*/}
</div>
);
};

View File

@ -1,63 +0,0 @@
import React, { useRef, useEffect } from "react";
import { motion, useInView, useAnimation } from "framer-motion";
interface Props {
children: React.ReactNode;
}
export const RevealT = ({ children }: Props) => {
const ref = useRef(null);
const isInView = useInView(ref, { once: false });
const mainControls = useAnimation();
const slideControls = useAnimation();
useEffect(() => {
if (isInView) {
mainControls.start("visible");
slideControls.start("visible");
} else {
mainControls.start("hidden");
}
}, [isInView]);
return (
<div ref={ref} className="relative overflow-hidden">
<motion.div
variants={{
hidden: { opacity: 0, y: -75 }, // 👈 muncul dari atas
visible: { opacity: 1, y: 0 },
}}
initial="hidden"
animate={mainControls}
transition={{
duration: 1,
delay: 0.1,
}}
>
{children}
</motion.div>
{/* Optional: Slide Overlay Animation */}
{/*
<motion.div
variants={{
hidden: { top: 0 },
visible: { top: "100%" },
}}
initial="hidden"
animate={slideControls}
transition={{ duration: 0.5, ease: "easeIn" }}
style={{
position: "absolute",
left: 0,
right: 0,
top: 0,
bottom: 0,
background: "#5e84ff",
zIndex: 20,
}}
/>
*/}
</div>
);
};

View File

@ -238,13 +238,13 @@ export function getMenuList(pathname: string, t: any): Group[] {
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/tag",
label: "Tag",
active: pathname === "/admin/settings/tag",
icon: "heroicons:arrow-trending-up",
children: [],
},
{
href: "/admin/settings/banner",
label: "Banner",

View File

@ -83,14 +83,19 @@ export async function getListCompetencies() {
return httpGetInterceptor(url);
}
export async function updateUserInternal(id: number, data: any) {
const url = `users/${id}`;
return httpPutInterceptor(url, data);
}
export async function getListExperiences() {
const url = "users/user-experiences/list";
return httpGetInterceptor(url);
}
export async function updateUserInternal(id: number, data: any) {
const url = `users/${id}`;
return httpPutInterceptor(url, data);
export async function saveUserInternal(data: any) {
const url = "users/save";
return httpPostInterceptor(url, data);
}
export async function checkRolePlacementsAvailability(data: any) {