diff --git a/components/landing-page/category.tsx b/components/landing-page/category.tsx index 1bd0eba..70c4447 100644 --- a/components/landing-page/category.tsx +++ b/components/landing-page/category.tsx @@ -6,6 +6,7 @@ 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"); @@ -21,7 +22,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); } @@ -55,56 +56,58 @@ export default function Category() { categories.length > 0 ? categories : fallbackCategories; return ( -
-
-

- {loading - ? t("loadCategory") - : `${displayCategories.length} ${t("category")}`} -

+ +
+
+

+ {loading + ? t("loadCategory") + : `${displayCategories.length} ${t("category")}`} +

- {loading ? ( - // Loading skeleton -
- {Array.from({ length: 10 }).map((_, index) => ( -
-
-
- ))} -
- ) : ( -
- {displayCategories.map((category, index) => { - // Handle both API data and fallback data - const categoryTitle = - typeof category === "string" ? category : category.title; - const categorySlug = - typeof category === "string" - ? category.toLowerCase().replace(/\s+/g, "-") - : category.slug; - - return ( - - ); - })} -
- )} -
-
+
+
+ ))} + + ) : ( +
+ {displayCategories.map((category, index) => { + // Handle both API data and fallback data + const categoryTitle = + typeof category === "string" ? category : category.title; + const categorySlug = + typeof category === "string" + ? category.toLowerCase().replace(/\s+/g, "-") + : category.slug; + + return ( + + ); + })} +
+ )} + +
+ ); } diff --git a/components/landing-page/footer.tsx b/components/landing-page/footer.tsx index 2599ae9..6746351 100644 --- a/components/landing-page/footer.tsx +++ b/components/landing-page/footer.tsx @@ -13,6 +13,7 @@ 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 = ` @@ -100,178 +101,180 @@ export default function Footer() { }, []); return ( - + + ); } diff --git a/components/landing-page/header.tsx b/components/landing-page/header.tsx index e12c9a5..2971cb7 100644 --- a/components/landing-page/header.tsx +++ b/components/landing-page/header.tsx @@ -18,6 +18,9 @@ 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"]; @@ -41,7 +44,7 @@ export default function Header() { undefined, undefined, "createdAt", - slug + slug, ); let articlesData: any[] = []; @@ -56,14 +59,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, ); } @@ -106,14 +109,14 @@ export default function Header() { const ids = new Set( (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) { @@ -128,75 +131,77 @@ export default function Header() { if (bookmarkedIds.size > 0) { localStorage.setItem( "bookmarkedIds", - JSON.stringify(Array.from(bookmarkedIds)) + JSON.stringify(Array.from(bookmarkedIds)), ); } }, [bookmarkedIds]); return ( -
-
- {data.length > 0 && ( - - setBookmarkedIds((prev) => new Set([...prev, Number(id)])) - } - /> - )} - -
- {data.slice(1, 5).map((item) => ( + +
+
+ {data.length > 0 && ( setBookmarkedIds((prev) => new Set([...prev, Number(id)])) } /> - ))} -
-
+ )} -
- - {images.map((img, index) => ( - -
- {/* + {data.slice(1, 5).map((item) => ( + + setBookmarkedIds((prev) => new Set([...prev, Number(id)])) + } + /> + ))} +
+
+ +
+ + {images.map((img, index) => ( + +
+ {/* {`slide-${index}`} */} - -
-
- ))} -
-
-
+ + + + ))} + + + + ); } @@ -218,12 +223,26 @@ function Card({ const [isBookmarked, setIsBookmarked] = useState(isInitiallyBookmarked); const DEFAULT_IMAGE = "/assets/logo1.png"; - const [imageSrc, setImageSrc] = useState( - item?.smallThumbnailLink || DEFAULT_IMAGE - ); + const [imageSrc, setImageSrc] = useState(DEFAULT_IMAGE); useEffect(() => { - setImageSrc(item?.smallThumbnailLink || DEFAULT_IMAGE); + 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); + }; }, [item?.smallThumbnailLink]); useEffect(() => { @@ -266,7 +285,7 @@ function Card({ newSet.add(Number(item.id)); localStorage.setItem( "bookmarkedIds", - JSON.stringify(Array.from(newSet)) + JSON.stringify(Array.from(newSet)), ); MySwal.fire({ @@ -292,7 +311,7 @@ function Card({ }; return ( -
+
*/} - {item.title} setImageSrc(DEFAULT_IMAGE)} + className="w-full h-full object-contain" />
@@ -379,7 +396,7 @@ function Card({
- + ); } diff --git a/components/landing-page/media-update.tsx b/components/landing-page/media-update.tsx index f0093cf..2e2ab9e 100644 --- a/components/landing-page/media-update.tsx +++ b/components/landing-page/media-update.tsx @@ -17,6 +17,8 @@ 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 ""; @@ -340,61 +342,74 @@ export default function MediaUpdate() { }; function SafeImage({ src, alt }: { src?: string; alt?: string }) { - const [imgSrc, setImgSrc] = useState(src || DEFAULT_IMAGE); + const [imgSrc, setImgSrc] = useState(DEFAULT_IMAGE); useEffect(() => { - setImgSrc(src || DEFAULT_IMAGE); + if (!src) { + setImgSrc(DEFAULT_IMAGE); + return; + } + + const img = new window.Image(); + img.src = src; + + img.onload = () => { + setImgSrc(src); + }; + + img.onerror = () => { + setImgSrc(DEFAULT_IMAGE); + }; }, [src]); return ( - {alt setImgSrc(DEFAULT_IMAGE)} + className="w-full h-full object-contain" /> ); } return ( -
-
-

- {t("title")} -

+ +
+
+

+ {t("title")} +

- {/* Main Tab */} -
- - - - -
+ {/* Main Tab */} +
+ + + + +
- {/* Content Type Filter */} -
-
-
- {/* */} - - - - + + + + +
-
- {/* Slider */} - {loading ? ( -

{t("loadContent")}

- ) : ( - - {filteredData.map((item) => ( - -
- {/* ✅ Kondisi: jika typeId = 3 (text) atau 4 (audio) tampilkan ikon, lainnya tampilkan thumbnail */} - {item.typeId === 3 ? ( - // 📝 TEXT -
- - - -
- ) : item.typeId === 4 ? ( - // 🎵 AUDIO -
- - - -
- ) : ( - // 🎬 FOTO / VIDEO (default) -
- - + {/* Slider */} + {loading ? ( +

{t("loadContent")}

+ ) : ( + + {filteredData.map((item) => ( + +
+ {/* ✅ Kondisi: jika typeId = 3 (text) atau 4 (audio) tampilkan ikon, lainnya tampilkan thumbnail */} + {item.typeId === 3 ? ( + // 📝 TEXT +
+ + + +
+ ) : item.typeId === 4 ? ( + // 🎵 AUDIO +
+ + + +
+ ) : ( + // 🎬 FOTO / VIDEO (default) +
+ + - {/* {item.title */} - -
- )} - - {/* Caption / info */} -
-
- - {item.clientName} - - - {item.categories - ?.map((cat: any) => cat.title) - .join(", ")} - -
-

- {formatTanggal(item.createdAt)} -

- -

- {item.title} -

- - -
-
- - + +
+ )} + + {/* Caption / info */} +
+
+ + {item.clientName} + + + {item.categories + ?.map((cat: any) => cat.title) + .join(", ")} + +
+

+ {formatTanggal(item.createdAt)} +

+ +

+ {item.title} +

+ + +
+
+ + +
+
-
-
- - ))} - - )} + + ))} + + )} - {/* Lihat lebih banyak - hanya muncul jika ada data */} - {filteredData.length > 0 && ( -
- - - -
- )} -
-
+ {/* Lihat lebih banyak - hanya muncul jika ada data */} + {filteredData.length > 0 && ( +
+ + + +
+ )} +
+
+ ); } diff --git a/components/landing-page/navbar.tsx b/components/landing-page/navbar.tsx index 1f3e8f6..8e7c007 100644 --- a/components/landing-page/navbar.tsx +++ b/components/landing-page/navbar.tsx @@ -14,6 +14,7 @@ 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"); @@ -75,217 +76,218 @@ export default function Navbar() { const fullname = Cookies.get("ufne"); return ( -
-
- setIsSidebarOpen(true)} - /> - - - Logo +
+
+ setIsSidebarOpen(true)} /> - - + + Logo + -
- + + +
+ +
-
- {/* 🌐 NAV MENU */} - + +  Dashboard + - {/* 📱 SIDEBAR MOBILE */} - {isSidebarOpen && ( -
-
- + +
+ )} +
+ )} + -
-

- {t("language")} -

- {/* button language */} -
- -
- {/*
+ {/* 📱 SIDEBAR MOBILE */} + {isSidebarOpen && ( +
+
+ + +
+

+ {t("language")} +

+ {/* button language */} +
+ +
+ {/*
*/} -
- -
-

- {t("features")} -

-
- {NAV_ITEMS.map((item) => ( - - ))}
-
-
- - {t("about")} - - - {t("advertising")} - - - {t("contact")} - +
+

+ {t("features")} +

+
+ {NAV_ITEMS.map((item) => ( + + ))} +
+
- {!isLoggedIn ? ( - <> - - {t("login")} - - - {t("register")} - - - ) : ( - - )} + {t("about")} + + + {t("advertising")} + + + {t("contact")} + + + {!isLoggedIn ? ( + <> + + {t("login")} + + + {t("register")} + + + ) : ( + + )} +
+ + +

+ {t("subscribeTitle")} +

+ + +
- -

- {t("subscribeTitle")} -

- - -
+
setIsSidebarOpen(false)} + />
- -
setIsSidebarOpen(false)} - /> -
- )} -
+ )} +
+ ); } diff --git a/components/main/content/image-detail.tsx b/components/main/content/image-detail.tsx index 1da0258..2042faf 100644 --- a/components/main/content/image-detail.tsx +++ b/components/main/content/image-detail.tsx @@ -93,17 +93,17 @@ export default function ImageDetail({ id }: { id: number }) { files: article.files?.map((f: any) => ({ id: f.id, - 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, + url: f.fileUrl, + fileName: f.fileName, + filePath: f.filePath, + fileThumbnail: f.fileThumbnail, + fileAlt: f.fileAlt, + widthPixel: f.widthPixel, + heightPixel: f.heightPixel, size: f.size, - downloadCount: f.download_count, - createdAt: f.created_at, - updatedAt: f.updated_at, + downloadCount: f.downloadCount, + createdAt: f.createdAt, + updatedAt: f.updatedAt, })) || [], }; @@ -155,7 +155,7 @@ export default function ImageDetail({ id }: { id: number }) {
Main setSelectedImage(index)} key={file?.id}> - {data.viewCount || 0} + {data.clickCount || 0}{" "} Creator: {data.creatorGroupLevelName} diff --git a/components/ui/Reveal.tsx b/components/ui/Reveal.tsx new file mode 100644 index 0000000..66b3851 --- /dev/null +++ b/components/ui/Reveal.tsx @@ -0,0 +1,58 @@ +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 ( +
+ + {children} + + {/* TODO green slide thingy */} + {/* */} +
+ ); +}; diff --git a/components/ui/RevealL.tsx b/components/ui/RevealL.tsx new file mode 100644 index 0000000..45bb24a --- /dev/null +++ b/components/ui/RevealL.tsx @@ -0,0 +1,63 @@ +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 ( +
+ + {children} + + + {/* Optional: Slide Overlay Animation */} + {/* + + */} +
+ ); +}; diff --git a/components/ui/RevealR.tsx b/components/ui/RevealR.tsx new file mode 100644 index 0000000..42356ce --- /dev/null +++ b/components/ui/RevealR.tsx @@ -0,0 +1,63 @@ +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 ( +
+ + {children} + + + {/* Optional: Slide Overlay Animation */} + {/* + + */} +
+ ); +}; diff --git a/components/ui/RevealT.tsx b/components/ui/RevealT.tsx new file mode 100644 index 0000000..ed46ef2 --- /dev/null +++ b/components/ui/RevealT.tsx @@ -0,0 +1,63 @@ +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 ( +
+ + {children} + + + {/* Optional: Slide Overlay Animation */} + {/* + + */} +
+ ); +};