fix: add reveal animation in landing
continuous-integration/drone/push Build is passing Details

This commit is contained in:
Sabda Yagra 2026-03-06 11:19:17 +07:00
parent a71cbeefeb
commit 24b44a491a
10 changed files with 1068 additions and 767 deletions

View File

@ -6,6 +6,7 @@ import {
ArticleCategory, ArticleCategory,
} from "@/service/categories/article-categories"; } from "@/service/categories/article-categories";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { RevealR } from "../ui/RevealR";
export default function Category() { export default function Category() {
const t = useTranslations("MediaUpdate"); const t = useTranslations("MediaUpdate");
@ -21,7 +22,7 @@ export default function Category() {
// Filter hanya kategori yang aktif dan published // Filter hanya kategori yang aktif dan published
const activeCategories = response.data.data.filter( const activeCategories = response.data.data.filter(
(category: ArticleCategory) => (category: ArticleCategory) =>
category.isActive && category.isPublish category.isActive && category.isPublish,
); );
setCategories(activeCategories); setCategories(activeCategories);
} }
@ -55,56 +56,58 @@ export default function Category() {
categories.length > 0 ? categories : fallbackCategories; categories.length > 0 ? categories : fallbackCategories;
return ( return (
<section className="px-4 py-10"> <RevealR>
<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"> <section className="px-4 py-10">
<h2 className="text-xl font-semibold mb-5"> <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">
{loading <h2 className="text-xl font-semibold mb-5">
? t("loadCategory") {loading
: `${displayCategories.length} ${t("category")}`} ? t("loadCategory")
</h2> : `${displayCategories.length} ${t("category")}`}
</h2>
{loading ? ( {loading ? (
// Loading skeleton // Loading skeleton
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
{Array.from({ length: 10 }).map((_, index) => ( {Array.from({ length: 10 }).map((_, index) => (
<div <div
key={index}
className="px-4 py-2 rounded border border-gray-200 bg-gray-100 animate-pulse"
>
<div className="h-4 w-20 bg-gray-300 rounded"></div>
</div>
))}
</div>
) : (
<div className="flex flex-wrap gap-3">
{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 (
<button
key={index} key={index}
className="px-4 py-2 rounded border border-gray-300 text-gray-700 dark:text-white text-sm font-medium hover:bg-gray-100 hover:border-gray-400 transition-all duration-200" className="px-4 py-2 rounded border border-gray-200 bg-gray-100 animate-pulse"
onClick={() => {
// Navigate to category page or search by category
console.log(
`Category clicked: ${categoryTitle} (${categorySlug})`
);
// TODO: Implement navigation to category page
}}
> >
{categoryTitle} <div className="h-4 w-20 bg-gray-300 rounded"></div>
</button> </div>
); ))}
})} </div>
</div> ) : (
)} <div className="flex flex-wrap gap-3">
</div> {displayCategories.map((category, index) => {
</section> // 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 (
<button
key={index}
className="px-4 py-2 rounded border border-gray-300 text-gray-700 dark:text-white text-sm font-medium hover:bg-gray-100 hover:border-gray-400 transition-all duration-200"
onClick={() => {
// Navigate to category page or search by category
console.log(
`Category clicked: ${categoryTitle} (${categorySlug})`,
);
// TODO: Implement navigation to category page
}}
>
{categoryTitle}
</button>
);
})}
</div>
)}
</div>
</section>
</RevealR>
); );
} }

View File

@ -13,6 +13,7 @@ import "swiper/css";
import "swiper/css/navigation"; import "swiper/css/navigation";
import LocalSwitcher from "../partials/header/locale-switcher"; import LocalSwitcher from "../partials/header/locale-switcher";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { Reveal } from "./Reveal";
// Custom styles for Swiper // Custom styles for Swiper
const swiperStyles = ` const swiperStyles = `
@ -100,178 +101,180 @@ export default function Footer() {
}, []); }, []);
return ( return (
<footer className="border-t bg-white dark:bg-default-50 text-center"> <Reveal>
<style jsx>{swiperStyles}</style> <footer className="border-t bg-white dark:bg-default-50 text-center">
<div className="max-w-[1350px] mx-auto"> <style jsx>{swiperStyles}</style>
<div className="py-6"> <div className="max-w-[1350px] mx-auto">
<h2 className="text-2xl font-semibold mb-4 px-4 md:px-0"> <div className="py-6">
{t("publication")} <h2 className="text-2xl font-semibold mb-4 px-4 md:px-0">
</h2> {t("publication")}
<div className="px-4 md:px-12"> </h2>
<Swiper <div className="px-4 md:px-12">
modules={[Navigation, Autoplay]} <Swiper
spaceBetween={24} modules={[Navigation, Autoplay]}
slidesPerView="auto" spaceBetween={24}
centeredSlides={clients.length <= 4} slidesPerView="auto"
navigation={{ centeredSlides={clients.length <= 4}
nextEl: ".swiper-button-next", navigation={{
prevEl: ".swiper-button-prev", nextEl: ".swiper-button-next",
}} prevEl: ".swiper-button-prev",
autoplay={{ }}
delay: 3000, autoplay={{
disableOnInteraction: false, delay: 3000,
}} disableOnInteraction: false,
loop={clients.length > 4} }}
className={`client-swiper ${ loop={clients.length > 4}
clients.length <= 4 ? "swiper-centered" : "" className={`client-swiper ${
}`} clients.length <= 4 ? "swiper-centered" : ""
> }`}
{loading
? // Loading skeleton
Array.from({ length: 8 }).map((_, idx) => (
<SwiperSlide key={idx} className="!w-auto">
<div className="w-[80px] h-[80px] md:w-[100px] md:h-[100px] bg-gray-200 rounded animate-pulse" />
</SwiperSlide>
))
: clients.length > 0
? // Dynamic clients from API
clients.map((client, idx) => (
<SwiperSlide key={idx} className="!w-auto">
<a
href={`/in/tenant/${client.slug}`}
target="_blank"
rel="noopener noreferrer"
className="group block"
>
{client.logoUrl ? (
<Image
src={client.logoUrl}
alt={client.name}
width={100}
height={100}
className="md:w-[100px] md:h-[100px] object-contain hover:opacity-80 transition"
/>
) : (
// Fallback when no logo - menggunakan placeholder image
<div className="w-[80px] h-[80px] md:w-[100px] md:h-[100px] rounded flex items-center justify-center hover:from-blue-200 hover:to-blue-300 transition-all duration-200">
<Image
src="/logo-netidhub.png"
alt={`${client.name} placeholder`}
width={100}
height={100}
className="md:w-[100px] md:h-[100px] object-contain opacity-70"
/>
</div>
)}
</a>
</SwiperSlide>
))
: // Fallback to static logos if API fails or no data
logos.map((logo, idx) => (
<SwiperSlide key={idx} className="!w-auto">
<a
href={`/in/tenant/${logo.slug}`}
target="_blank"
rel="noopener noreferrer"
className="block"
>
<Image
src={logo.src}
alt={`logo-${idx}`}
width={80}
height={80}
className="md:w-[100px] md:h-[100px] object-contain hover:opacity-80 transition"
/>
</a>
</SwiperSlide>
))}
</Swiper>
{/* Navigation Buttons */}
{/* <div className="swiper-button-prev !text-gray-600 !w-8 !h-8 !mt-0 !top-1/2 !left-0 !-translate-y-1/2"></div> */}
{/* <div className="swiper-button-next !text-gray-600 !w-8 !h-8 !mt-0 !top-1/2 !right-0 !-translate-y-1/2"></div> */}
</div>
</div>
<div className="border-t my-6 w-full max-w-6xl mx-auto" />
<div className="flex flex-col md:flex-row items-center justify-between gap-4 px-4 pb-6 max-w-6xl mx-auto text-sm text-gray-600 dark:text-white">
<div className="flex items-center gap-2">
<span>ver 1.0.0 @2025 - {t("netidhub")}</span>
<Image
src="/qudo.png"
alt="qudoco"
width={80}
height={80}
className="object-contain"
/>
</div>
{/* Social Media */}
<div className="flex gap-3 text-gray-800 dark:text-white">
<Instagram className="hover:text-black" />
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M14 13.5h2.5l1-4H14v-2c0-1.03 0-2 2-2h1.5V2.14c-.326-.043-1.557-.14-2.857-.14C11.928 2 10 3.657 10 6.7v2.8H7v4h3V22h4z"
/>
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<g fill="none">
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
<path
fill="currentColor"
d="M4.594 4.984a1 1 0 0 1 .941.429C7.011 7.572 8.783 8.47 10.75 8.674c.096-.841.323-1.672.75-2.404c.626-1.074 1.644-1.864 3.098-2.156c2.01-.404 3.54.324 4.427 1.215l1.792-.335a1 1 0 0 1 1.053 1.478l-1.72 3.022c.157 4.361-1.055 7.405-3.639 9.502c-1.37 1.112-3.332 1.743-5.485 1.938c-2.17.196-4.623-.041-7.061-.753a1 1 0 0 1 .007-1.922c1.226-.349 2.16-.65 3.003-1.177c-1.199-.636-2.082-1.468-2.707-2.416c-.868-1.318-1.19-2.788-1.254-4.113S3.141 8 3.343 7.115c.115-.505.249-1.011.434-1.495a1 1 0 0 1 .818-.636Z"
/>
</g>
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
// fill-rule="evenodd"
d="M9.935 14.628v-5.62l5.403 2.82zM21.8 8.035s-.195-1.379-.795-1.986c-.76-.796-1.613-.8-2.004-.847C16.203 5 12.004 5 12.004 5h-.008s-4.198 0-6.997.202c-.391.047-1.243.05-2.004.847c-.6.607-.795 1.986-.795 1.986S2 9.653 2 11.272v1.517c0 1.618.2 3.237.2 3.237s.195 1.378.795 1.985c.76.797 1.76.771 2.205.855c1.6.153 6.8.2 6.8.2s4.203-.006 7.001-.208c.391-.047 1.244-.05 2.004-.847c.6-.607.795-1.985.795-1.985s.2-1.619.2-3.237v-1.517c0-1.619-.2-3.237-.2-3.237"
/>
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<g
fill="none"
// fill-rule="evenodd"
> >
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" /> {loading
<path ? // Loading skeleton
fill="currentColor" Array.from({ length: 8 }).map((_, idx) => (
d="M14 2a2 2 0 0 1 2 2a3.004 3.004 0 0 0 2.398 2.94a2 2 0 0 1-.796 3.92A7 7 0 0 1 16 10.325V16a6 6 0 1 1-7.499-5.81a2 2 0 0 1 .998 3.873A2.002 2.002 0 0 0 10 18a2 2 0 0 0 2-2V4a2 2 0 0 1 2-2" <SwiperSlide key={idx} className="!w-auto">
/> <div className="w-[80px] h-[80px] md:w-[100px] md:h-[100px] bg-gray-200 rounded animate-pulse" />
</g> </SwiperSlide>
</svg> ))
: clients.length > 0
? // Dynamic clients from API
clients.map((client, idx) => (
<SwiperSlide key={idx} className="!w-auto">
<a
href={`/in/tenant/${client.slug}`}
target="_blank"
rel="noopener noreferrer"
className="group block"
>
{client.logoUrl ? (
<Image
src={client.logoUrl}
alt={client.name}
width={100}
height={100}
className="md:w-[100px] md:h-[100px] object-contain hover:opacity-80 transition"
/>
) : (
// Fallback when no logo - menggunakan placeholder image
<div className="w-[80px] h-[80px] md:w-[100px] md:h-[100px] rounded flex items-center justify-center hover:from-blue-200 hover:to-blue-300 transition-all duration-200">
<Image
src="/logo-netidhub.png"
alt={`${client.name} placeholder`}
width={100}
height={100}
className="md:w-[100px] md:h-[100px] object-contain opacity-70"
/>
</div>
)}
</a>
</SwiperSlide>
))
: // Fallback to static logos if API fails or no data
logos.map((logo, idx) => (
<SwiperSlide key={idx} className="!w-auto">
<a
href={`/in/tenant/${logo.slug}`}
target="_blank"
rel="noopener noreferrer"
className="block"
>
<Image
src={logo.src}
alt={`logo-${idx}`}
width={80}
height={80}
className="md:w-[100px] md:h-[100px] object-contain hover:opacity-80 transition"
/>
</a>
</SwiperSlide>
))}
</Swiper>
{/* Navigation Buttons */}
{/* <div className="swiper-button-prev !text-gray-600 !w-8 !h-8 !mt-0 !top-1/2 !left-0 !-translate-y-1/2"></div> */}
{/* <div className="swiper-button-next !text-gray-600 !w-8 !h-8 !mt-0 !top-1/2 !right-0 !-translate-y-1/2"></div> */}
</div>
</div> </div>
{/* button language */} <div className="border-t my-6 w-full max-w-6xl mx-auto" />
<div className={`relative text-left border rounded-lg`}>
<LocalSwitcher /> <div className="flex flex-col md:flex-row items-center justify-between gap-4 px-4 pb-6 max-w-6xl mx-auto text-sm text-gray-600 dark:text-white">
<div className="flex items-center gap-2">
<span>ver 1.0.0 @2025 - {t("netidhub")}</span>
<Image
src="/qudo.png"
alt="qudoco"
width={80}
height={80}
className="object-contain"
/>
</div>
{/* Social Media */}
<div className="flex gap-3 text-gray-800 dark:text-white">
<Instagram className="hover:text-black" />
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M14 13.5h2.5l1-4H14v-2c0-1.03 0-2 2-2h1.5V2.14c-.326-.043-1.557-.14-2.857-.14C11.928 2 10 3.657 10 6.7v2.8H7v4h3V22h4z"
/>
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<g fill="none">
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
<path
fill="currentColor"
d="M4.594 4.984a1 1 0 0 1 .941.429C7.011 7.572 8.783 8.47 10.75 8.674c.096-.841.323-1.672.75-2.404c.626-1.074 1.644-1.864 3.098-2.156c2.01-.404 3.54.324 4.427 1.215l1.792-.335a1 1 0 0 1 1.053 1.478l-1.72 3.022c.157 4.361-1.055 7.405-3.639 9.502c-1.37 1.112-3.332 1.743-5.485 1.938c-2.17.196-4.623-.041-7.061-.753a1 1 0 0 1 .007-1.922c1.226-.349 2.16-.65 3.003-1.177c-1.199-.636-2.082-1.468-2.707-2.416c-.868-1.318-1.19-2.788-1.254-4.113S3.141 8 3.343 7.115c.115-.505.249-1.011.434-1.495a1 1 0 0 1 .818-.636Z"
/>
</g>
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
// fill-rule="evenodd"
d="M9.935 14.628v-5.62l5.403 2.82zM21.8 8.035s-.195-1.379-.795-1.986c-.76-.796-1.613-.8-2.004-.847C16.203 5 12.004 5 12.004 5h-.008s-4.198 0-6.997.202c-.391.047-1.243.05-2.004.847c-.6.607-.795 1.986-.795 1.986S2 9.653 2 11.272v1.517c0 1.618.2 3.237.2 3.237s.195 1.378.795 1.985c.76.797 1.76.771 2.205.855c1.6.153 6.8.2 6.8.2s4.203-.006 7.001-.208c.391-.047 1.244-.05 2.004-.847c.6-.607.795-1.985.795-1.985s.2-1.619.2-3.237v-1.517c0-1.619-.2-3.237-.2-3.237"
/>
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<g
fill="none"
// fill-rule="evenodd"
>
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
<path
fill="currentColor"
d="M14 2a2 2 0 0 1 2 2a3.004 3.004 0 0 0 2.398 2.94a2 2 0 0 1-.796 3.92A7 7 0 0 1 16 10.325V16a6 6 0 1 1-7.499-5.81a2 2 0 0 1 .998 3.873A2.002 2.002 0 0 0 10 18a2 2 0 0 0 2-2V4a2 2 0 0 1 2-2"
/>
</g>
</svg>
</div>
{/* button language */}
<div className={`relative text-left border rounded-lg`}>
<LocalSwitcher />
</div>
</div> </div>
</div> </div>
</div> </footer>
</footer> </Reveal>
); );
} }

View File

@ -18,6 +18,9 @@ import "swiper/css/navigation";
import "swiper/css/pagination"; import "swiper/css/pagination";
import ImageBlurry from "../ui/image-blurry"; import ImageBlurry from "../ui/image-blurry";
import { useTranslations } from "next-intl"; 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"]; const images = ["/PPS.png", "/PPS2.jpeg", "/PPS3.jpg", "/PPS4.png"];
@ -41,7 +44,7 @@ export default function Header() {
undefined, undefined,
undefined, undefined,
"createdAt", "createdAt",
slug slug,
); );
let articlesData: any[] = []; let articlesData: any[] = [];
@ -56,14 +59,14 @@ export default function Header() {
"createdAt", "createdAt",
"", "",
"", "",
"" "",
); );
articlesData = (fallbackResponse?.data?.data?.content || []).filter( articlesData = (fallbackResponse?.data?.data?.content || []).filter(
(item: any) => item.typeId === 1 (item: any) => item.typeId === 1,
); );
} else { } else {
articlesData = (response?.data?.data || []).filter( 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<number>( const ids = new Set<number>(
(Array.isArray(bookmarks) ? bookmarks : []) (Array.isArray(bookmarks) ? bookmarks : [])
.map((b: any) => Number(b.articleId ?? b.id ?? b.article?.id)) .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]); const merged = new Set([...localSet, ...ids]);
setBookmarkedIds(merged); setBookmarkedIds(merged);
localStorage.setItem( localStorage.setItem(
"bookmarkedIds", "bookmarkedIds",
JSON.stringify(Array.from(merged)) JSON.stringify(Array.from(merged)),
); );
} }
} catch (error) { } catch (error) {
@ -128,75 +131,77 @@ export default function Header() {
if (bookmarkedIds.size > 0) { if (bookmarkedIds.size > 0) {
localStorage.setItem( localStorage.setItem(
"bookmarkedIds", "bookmarkedIds",
JSON.stringify(Array.from(bookmarkedIds)) JSON.stringify(Array.from(bookmarkedIds)),
); );
} }
}, [bookmarkedIds]); }, [bookmarkedIds]);
return ( return (
<section className="max-w-[1350px] mx-auto px-4"> <RevealR>
<div className="flex flex-col lg:flex-row gap-6 py-6"> <section className="max-w-[1350px] mx-auto px-4">
{data.length > 0 && ( <div className="flex flex-col lg:flex-row gap-6 py-6">
<Card {data.length > 0 && (
item={data[0]}
isBig
isInitiallyBookmarked={bookmarkedIds.has(Number(data[0].id))}
onSaved={(id) =>
setBookmarkedIds((prev) => new Set([...prev, Number(id)]))
}
/>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 w-full">
{data.slice(1, 5).map((item) => (
<Card <Card
key={item.id} item={data[0]}
item={item} isBig
isInitiallyBookmarked={bookmarkedIds.has(Number(item.id))} isInitiallyBookmarked={bookmarkedIds.has(Number(data[0].id))}
onSaved={(id) => onSaved={(id) =>
setBookmarkedIds((prev) => new Set([...prev, Number(id)])) setBookmarkedIds((prev) => new Set([...prev, Number(id)]))
} }
/> />
))} )}
</div>
</div>
<div className="relative w-full h-48 sm:h-64 md:h-80 lg:h-[460px] mt-4 rounded-xl overflow-hidden"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 w-full">
<Swiper {data.slice(1, 5).map((item) => (
modules={[Navigation, Pagination]} <Card
navigation key={item.id}
pagination={{ clickable: true }} item={item}
spaceBetween={10} isInitiallyBookmarked={bookmarkedIds.has(Number(item.id))}
slidesPerView={1} onSaved={(id) =>
loop={true} setBookmarkedIds((prev) => new Set([...prev, Number(id)]))
className="w-full h-full" }
> />
{images.map((img, index) => ( ))}
<SwiperSlide key={index}> </div>
<div className="relative w-full h-48 sm:h-64 md:h-80 lg:h-[460px]"> </div>
{/* <Image
<div className="relative w-full h-48 sm:h-64 md:h-80 lg:h-[460px] mt-4 rounded-xl overflow-hidden">
<Swiper
modules={[Navigation, Pagination]}
navigation
pagination={{ clickable: true }}
spaceBetween={10}
slidesPerView={1}
loop={true}
className="w-full h-full"
>
{images.map((img, index) => (
<SwiperSlide key={index}>
<div className="relative w-full h-48 sm:h-64 md:h-80 lg:h-[460px]">
{/* <Image
src={img} src={img}
alt={`slide-${index}`} alt={`slide-${index}`}
fill fill
className="object-cover rounded-xl" className="object-cover rounded-xl"
priority={index === 0} priority={index === 0}
/> */} /> */}
<ImageBlurry <ImageBlurry
priority priority
src={img} src={img}
alt="gambar" alt="gambar"
style={{ style={{
objectFit: "contain", objectFit: "contain",
width: "100%", width: "100%",
height: "100%", height: "100%",
}} }}
/> />
</div> </div>
</SwiperSlide> </SwiperSlide>
))} ))}
</Swiper> </Swiper>
</div> </div>
</section> </section>
</RevealR>
); );
} }
@ -218,12 +223,26 @@ function Card({
const [isBookmarked, setIsBookmarked] = useState(isInitiallyBookmarked); const [isBookmarked, setIsBookmarked] = useState(isInitiallyBookmarked);
const DEFAULT_IMAGE = "/assets/logo1.png"; const DEFAULT_IMAGE = "/assets/logo1.png";
const [imageSrc, setImageSrc] = useState( const [imageSrc, setImageSrc] = useState(DEFAULT_IMAGE);
item?.smallThumbnailLink || DEFAULT_IMAGE
);
useEffect(() => { 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]); }, [item?.smallThumbnailLink]);
useEffect(() => { useEffect(() => {
@ -266,7 +285,7 @@ function Card({
newSet.add(Number(item.id)); newSet.add(Number(item.id));
localStorage.setItem( localStorage.setItem(
"bookmarkedIds", "bookmarkedIds",
JSON.stringify(Array.from(newSet)) JSON.stringify(Array.from(newSet)),
); );
MySwal.fire({ MySwal.fire({
@ -292,7 +311,7 @@ function Card({
}; };
return ( return (
<div> <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 ${ className={`rounded-xl overflow-hidden shadow hover:shadow-lg transition-all bg-white dark:bg-black dark:border dark:border-slate-50 ${
isBig isBig
@ -312,12 +331,10 @@ function Card({
fill fill
className="object-cover" className="object-cover"
/> */} /> */}
<Image <ImageBlurry
src={imageSrc} src={imageSrc}
alt={item.title} alt={item.title}
fill className="w-full h-full object-contain"
className="object-cover"
onError={() => setImageSrc(DEFAULT_IMAGE)}
/> />
</Link> </Link>
</div> </div>
@ -379,7 +396,7 @@ function Card({
</div> </div>
</div> </div>
</div> </div>
</div> </RevealL>
); );
} }

View File

@ -17,6 +17,8 @@ import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content"; import withReactContent from "sweetalert2-react-content";
import { ThumbsUp, ThumbsDown } from "lucide-react"; import { ThumbsUp, ThumbsDown } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import ImageBlurry from "../ui/image-blurry";
import { RevealL } from "../ui/RevealL";
function formatTanggal(dateString: string) { function formatTanggal(dateString: string) {
if (!dateString) return ""; if (!dateString) return "";
@ -340,61 +342,74 @@ export default function MediaUpdate() {
}; };
function SafeImage({ src, alt }: { src?: string; alt?: string }) { function SafeImage({ src, alt }: { src?: string; alt?: string }) {
const [imgSrc, setImgSrc] = useState(src || DEFAULT_IMAGE); const [imgSrc, setImgSrc] = useState(DEFAULT_IMAGE);
useEffect(() => { 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]); }, [src]);
return ( return (
<Image <ImageBlurry
src={imgSrc} src={imgSrc}
alt={alt || "Image"} alt={alt || "Image"}
fill className="w-full h-full object-contain"
className="object-cover cursor-pointer hover:opacity-90 transition-opacity"
onError={() => setImgSrc(DEFAULT_IMAGE)}
/> />
); );
} }
return ( return (
<section className="bg-white dark:bg-default-50 px-4 py-10 border max-w-[1350px] mx-auto rounded-md border-[#CDD5DF] my-10"> <RevealL>
<div className="max-w-screen-xl mx-auto"> <section className="bg-white dark:bg-default-50 px-4 py-10 border max-w-[1350px] mx-auto rounded-md border-[#CDD5DF] my-10">
<h2 className="text-2xl font-semibold text-center mb-6"> <div className="max-w-screen-xl mx-auto">
{t("title")} <h2 className="text-2xl font-semibold text-center mb-6">
</h2> {t("title")}
</h2>
{/* Main Tab */} {/* Main Tab */}
<div className="flex justify-center mb-6 bg-white dark:bg-default-50"> <div className="flex justify-center mb-6 bg-white dark:bg-default-50">
<Card className="bg-[#FFFFFF] dark:bg-default-50 rounded-xl flex flex-row p-3 gap-2 shadow-md border border-gray-200"> <Card className="bg-[#FFFFFF] dark:bg-default-50 rounded-xl flex flex-row p-3 gap-2 shadow-md border border-gray-200">
<button <button
onClick={() => setTab("latest")} onClick={() => setTab("latest")}
className={`px-6 py-3 rounded-lg text-sm font-semibold transition-all duration-200 ${ className={`px-6 py-3 rounded-lg text-sm font-semibold transition-all duration-200 ${
tab === "latest" tab === "latest"
? "bg-[#C6A455] text-white shadow-sm" ? "bg-[#C6A455] text-white shadow-sm"
: "text-[#C6A455] hover:bg-[#C6A455]/10" : "text-[#C6A455] hover:bg-[#C6A455]/10"
}`} }`}
> >
{t("latest")} {t("latest")}
</button> </button>
<button <button
onClick={() => setTab("popular")} onClick={() => setTab("popular")}
className={`px-6 py-3 rounded-lg text-sm font-semibold transition-all duration-200 ${ className={`px-6 py-3 rounded-lg text-sm font-semibold transition-all duration-200 ${
tab === "popular" tab === "popular"
? "bg-[#C6A455] text-white shadow-sm" ? "bg-[#C6A455] text-white shadow-sm"
: "text-[#C6A455] hover:bg-[#C6A455]/10" : "text-[#C6A455] hover:bg-[#C6A455]/10"
}`} }`}
> >
{t("popular")}{" "} {t("popular")}{" "}
</button> </button>
</Card> </Card>
</div> </div>
{/* Content Type Filter */} {/* Content Type Filter */}
<div className="flex justify-center mb-8"> <div className="flex justify-center mb-8">
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-2xl p-4 border-2 border-blue-100 shadow-lg dark:bg-default-50"> <div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-2xl p-4 border-2 border-blue-100 shadow-lg dark:bg-default-50">
<div className="flex flex-wrap justify-center gap-2"> <div className="flex flex-wrap justify-center gap-2">
{/* <button {/* <button
onClick={() => setContentType("all")} onClick={() => setContentType("all")}
className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${ className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${
contentType === "all" contentType === "all"
@ -405,180 +420,181 @@ export default function MediaUpdate() {
📋 Semua 📋 Semua
</button> */} </button> */}
<button <button
onClick={() => setContentType("foto")} onClick={() => setContentType("foto")}
className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${ className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${
contentType === "foto" contentType === "foto"
? "bg-gradient-to-r from-orange-500 to-red-600 text-white shadow-lg ring-2 ring-orange-300" ? "bg-gradient-to-r from-orange-500 to-red-600 text-white shadow-lg ring-2 ring-orange-300"
: "bg-white text-orange-600 border-2 border-orange-200 hover:border-orange-400 hover:shadow-md" : "bg-white text-orange-600 border-2 border-orange-200 hover:border-orange-400 hover:shadow-md"
}`} }`}
> >
📸 {t("image")} 📸 {t("image")}
</button> </button>
<button <button
onClick={() => setContentType("audiovisual")} onClick={() => setContentType("audiovisual")}
className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${ className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${
contentType === "audiovisual" contentType === "audiovisual"
? "bg-gradient-to-r from-purple-500 to-pink-600 text-white shadow-lg ring-2 ring-purple-300" ? "bg-gradient-to-r from-purple-500 to-pink-600 text-white shadow-lg ring-2 ring-purple-300"
: "bg-white text-purple-600 border-2 border-purple-200 hover:border-purple-400 hover:shadow-md" : "bg-white text-purple-600 border-2 border-purple-200 hover:border-purple-400 hover:shadow-md"
}`} }`}
> >
🎬 {t("video")} 🎬 {t("video")}
</button> </button>
<button <button
onClick={() => setContentType("audio")} onClick={() => setContentType("audio")}
className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${ className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${
contentType === "audio" contentType === "audio"
? "bg-gradient-to-r from-green-500 to-emerald-600 text-white shadow-lg ring-2 ring-green-300" ? "bg-gradient-to-r from-green-500 to-emerald-600 text-white shadow-lg ring-2 ring-green-300"
: "bg-white text-green-600 border-2 border-green-200 hover:border-green-400 hover:shadow-md" : "bg-white text-green-600 border-2 border-green-200 hover:border-green-400 hover:shadow-md"
}`} }`}
> >
🎵 Audio 🎵 Audio
</button> </button>
<button <button
onClick={() => setContentType("text")} onClick={() => setContentType("text")}
className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${ className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${
contentType === "text" contentType === "text"
? "bg-gradient-to-r from-gray-500 to-slate-600 text-white shadow-lg ring-2 ring-gray-300" ? "bg-gradient-to-r from-gray-500 to-slate-600 text-white shadow-lg ring-2 ring-gray-300"
: "bg-white text-gray-600 border-2 border-gray-200 hover:border-gray-400 hover:shadow-md" : "bg-white text-gray-600 border-2 border-gray-200 hover:border-gray-400 hover:shadow-md"
}`} }`}
> >
📝 {t("text")} 📝 {t("text")}
</button> </button>
</div>
</div> </div>
</div> </div>
</div>
{/* Slider */} {/* Slider */}
{loading ? ( {loading ? (
<p className="text-center">{t("loadContent")}</p> <p className="text-center">{t("loadContent")}</p>
) : ( ) : (
<Swiper <Swiper
modules={[Navigation]} modules={[Navigation]}
navigation navigation
spaceBetween={20} spaceBetween={20}
slidesPerView={1} slidesPerView={1}
breakpoints={{ breakpoints={{
640: { slidesPerView: 2 }, 640: { slidesPerView: 2 },
1024: { slidesPerView: 4 }, 1024: { slidesPerView: 4 },
}} }}
> >
{filteredData.map((item) => ( {filteredData.map((item) => (
<SwiperSlide key={item.id}> <SwiperSlide key={item.id}>
<div className="rounded-xl shadow-md overflow-hidden bg-white dark:bg-default-50 dark:border dark:border-slate-50"> <div className="rounded-xl shadow-md overflow-hidden bg-white dark:bg-default-50 dark:border dark:border-slate-50">
{/* ✅ Kondisi: jika typeId = 3 (text) atau 4 (audio) tampilkan ikon, lainnya tampilkan thumbnail */} {/* ✅ Kondisi: jika typeId = 3 (text) atau 4 (audio) tampilkan ikon, lainnya tampilkan thumbnail */}
{item.typeId === 3 ? ( {item.typeId === 3 ? (
// 📝 TEXT // 📝 TEXT
<div className="bg-[#e0c350] flex items-center justify-center h-[204px] text-white"> <div className="bg-[#e0c350] flex items-center justify-center h-[204px] text-white">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="90" width="90"
height="90" height="90"
viewBox="0 0 16 16" viewBox="0 0 16 16"
> >
<path <path
fill="currentColor" fill="currentColor"
d="M5 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V5.414a1.5 1.5 0 0 0-.44-1.06L9.647 1.439A1.5 1.5 0 0 0 8.586 1zM4 3a1 1 0 0 1 1-1h3v2.5A1.5 1.5 0 0 0 9.5 6H12v7a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1zm7.793 2H9.5a.5.5 0 0 1-.5-.5V2.207z" d="M5 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V5.414a1.5 1.5 0 0 0-.44-1.06L9.647 1.439A1.5 1.5 0 0 0 8.586 1zM4 3a1 1 0 0 1 1-1h3v2.5A1.5 1.5 0 0 0 9.5 6H12v7a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1zm7.793 2H9.5a.5.5 0 0 1-.5-.5V2.207z"
/> />
</svg> </svg>
</div> </div>
) : item.typeId === 4 ? ( ) : item.typeId === 4 ? (
// 🎵 AUDIO // 🎵 AUDIO
<div className="flex items-center justify-center bg-[#bb3523] w-full h-[204px] text-white"> <div className="flex items-center justify-center bg-[#bb3523] w-full h-[204px] text-white">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="100" width="100"
height="100" height="100"
viewBox="0 0 20 20" viewBox="0 0 20 20"
> >
<path <path
fill="currentColor" fill="currentColor"
d="M14.702 2.226A1 1 0 0 1 16 3.18v6.027a5.5 5.5 0 0 0-1-.184V6.18L8 8.368V15.5a2.5 2.5 0 1 1-1-2V5.368a1 1 0 0 1 .702-.955zM8 7.32l7-2.187V3.18L8 5.368zM5.5 14a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3m13.5.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0m-2.265-.436l-2.994-1.65a.5.5 0 0 0-.741.438v3.3a.5.5 0 0 0 .741.438l2.994-1.65a.5.5 0 0 0 0-.876" d="M14.702 2.226A1 1 0 0 1 16 3.18v6.027a5.5 5.5 0 0 0-1-.184V6.18L8 8.368V15.5a2.5 2.5 0 1 1-1-2V5.368a1 1 0 0 1 .702-.955zM8 7.32l7-2.187V3.18L8 5.368zM5.5 14a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3m13.5.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0m-2.265-.436l-2.994-1.65a.5.5 0 0 0-.741.438v3.3a.5.5 0 0 0 .741.438l2.994-1.65a.5.5 0 0 0 0-.876"
/> />
</svg> </svg>
</div> </div>
) : ( ) : (
// 🎬 FOTO / VIDEO (default) // 🎬 FOTO / VIDEO (default)
<div className="w-full h-[204px] relative"> <div className="w-full h-[204px] relative">
<Link href={getLink(item)}> <Link href={getLink(item)}>
<SafeImage <SafeImage
src={item.smallThumbnailLink} src={item.smallThumbnailLink}
alt={item.title} alt={item.title}
/> />
{/* <Image {/* <Image
src={item.smallThumbnailLink || "/placeholder.png"} src={item.smallThumbnailLink || "/placeholder.png"}
alt={item.title || "No Title"} alt={item.title || "No Title"}
fill fill
className="object-cover cursor-pointer hover:opacity-90 transition-opacity" className="object-cover cursor-pointer hover:opacity-90 transition-opacity"
/> */} /> */}
</Link> </Link>
</div> </div>
)} )}
{/* Caption / info */} {/* Caption / info */}
<div className="p-3"> <div className="p-3">
<div className="flex items-center gap-2 text-xs font-semibold flex-row justify-between mb-2"> <div className="flex items-center gap-2 text-xs font-semibold flex-row justify-between mb-2">
<span className="text-xs text-white px-2 py-0.5 rounded bg-emerald-600"> <span className="text-xs text-white px-2 py-0.5 rounded bg-emerald-600">
{item.clientName} {item.clientName}
</span> </span>
<span className="text-orange-600"> <span className="text-orange-600">
{item.categories {item.categories
?.map((cat: any) => cat.title) ?.map((cat: any) => cat.title)
.join(", ")} .join(", ")}
</span> </span>
</div> </div>
<p className="text-xs text-gray-500 mb-1"> <p className="text-xs text-gray-500 mb-1">
{formatTanggal(item.createdAt)} {formatTanggal(item.createdAt)}
</p> </p>
<Link href={getLink(item)}> <Link href={getLink(item)}>
<p className="text-sm font-semibold mb-3 line-clamp-2 cursor-pointer hover:text-blue-600 transition-colors"> <p className="text-sm font-semibold mb-3 line-clamp-2 cursor-pointer hover:text-blue-600 transition-colors">
{item.title} {item.title}
</p> </p>
</Link> </Link>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex gap-2 text-gray-600"> <div className="flex gap-2 text-gray-600">
<ThumbsUp className="w-4 h-4 cursor-pointer" /> <ThumbsUp className="w-4 h-4 cursor-pointer" />
<ThumbsDown className="w-4 h-4 cursor-pointer" /> <ThumbsDown className="w-4 h-4 cursor-pointer" />
</div>
<Button
onClick={() => handleSave(item.id)}
disabled={bookmarkedIds.has(Number(item.id))}
variant="default"
size="sm"
className={`rounded px-4 ${
bookmarkedIds.has(Number(item.id))
? "bg-gray-400 cursor-not-allowed text-white"
: "bg-red-700 text-white hover:bg-red-500"
}`}
>
{bookmarkedIds.has(Number(item.id))
? t("saved")
: t("save")}
</Button>
</div> </div>
<Button
onClick={() => handleSave(item.id)}
disabled={bookmarkedIds.has(Number(item.id))}
variant="default"
size="sm"
className={`rounded px-4 ${
bookmarkedIds.has(Number(item.id))
? "bg-gray-400 cursor-not-allowed text-white"
: "bg-red-700 text-white hover:bg-red-500"
}`}
>
{bookmarkedIds.has(Number(item.id))
? t("saved")
: t("save")}
</Button>
</div> </div>
</div> </div>
</div> </SwiperSlide>
</SwiperSlide> ))}
))} </Swiper>
</Swiper> )}
)}
{/* Lihat lebih banyak - hanya muncul jika ada data */} {/* Lihat lebih banyak - hanya muncul jika ada data */}
{filteredData.length > 0 && ( {filteredData.length > 0 && (
<div className="text-center mt-10"> <div className="text-center mt-10">
<Link href={getContentTypeLink()}> <Link href={getContentTypeLink()}>
<Button <Button
size={"lg"} size={"lg"}
className="text-[#b3882e] bg-transparent border border-[#b3882e] px-6 py-2 rounded-s-sm text-sm font-medium hover:bg-[#b3882e]/10 transition" className="text-[#b3882e] bg-transparent border border-[#b3882e] px-6 py-2 rounded-s-sm text-sm font-medium hover:bg-[#b3882e]/10 transition"
> >
{t("seeMore")} {t("seeMore")}
</Button> </Button>
</Link> </Link>
</div> </div>
)} )}
</div> </div>
</section> </section>
</RevealL>
); );
} }

View File

@ -14,6 +14,7 @@ import { DynamicLogoTenant } from "./dynamic-logo-tenant";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import LocalSwitcher from "../partials/header/locale-switcher"; import LocalSwitcher from "../partials/header/locale-switcher";
import ThemeSwitcher from "../partials/header/theme-switcher"; import ThemeSwitcher from "../partials/header/theme-switcher";
import { RevealT } from "../ui/RevealT";
export default function Navbar() { export default function Navbar() {
const t = useTranslations("Navbar"); const t = useTranslations("Navbar");
@ -75,217 +76,218 @@ export default function Navbar() {
const fullname = Cookies.get("ufne"); const fullname = Cookies.get("ufne");
return ( return (
<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"> <RevealT>
<div className="flex flex-row items-center justify-between space-x-4 z-10"> <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">
<Menu <div className="flex flex-row items-center justify-between space-x-4 z-10">
className="w-6 h-6 cursor-pointer" <Menu
onClick={() => setIsSidebarOpen(true)} className="w-6 h-6 cursor-pointer"
/> onClick={() => setIsSidebarOpen(true)}
<Link href="/" className="relative w-32 h-20">
<Image
src="/assets/logo1.png"
alt="Logo"
fill
className="object-contain"
/> />
</Link>
<DynamicLogoTenant /> <Link href="/" className="relative w-32 h-20">
<Image
src="/assets/logo1.png"
alt="Logo"
fill
className="object-contain"
/>
</Link>
<div className="hidden custom-lg-button:flex items-end"> <DynamicLogoTenant />
<ThemeSwitcher />
<div className="hidden custom-lg-button:flex items-end">
<ThemeSwitcher />
</div>
</div> </div>
</div>
{/* 🌐 NAV MENU */} {/* 🌐 NAV MENU */}
<nav className="absolute left-1/2 -translate-x-1/2 hidden md:flex space-x-3 lg:space-x-8 text-sm font-medium"> <nav className="absolute left-1/2 -translate-x-1/2 hidden md:flex space-x-3 lg:space-x-8 text-sm font-medium">
{filteredNavItems.map((item) => { {filteredNavItems.map((item) => {
const isActive = pathname === item.href; const isActive = pathname === item.href;
// 🔹 Pengecekan khusus untuk "Untuk Anda" // 🔹 Pengecekan khusus untuk "Untuk Anda"
const handleClick = (e: React.MouseEvent) => { const handleClick = (e: React.MouseEvent) => {
if (item.label === t("forYou")) { if (item.label === t("forYou")) {
e.preventDefault(); e.preventDefault();
if (!checkLoginStatus()) { if (!checkLoginStatus()) {
router.push("/auth"); router.push("/auth");
} else { } else {
router.push("/in/for-you"); router.push("/in/for-you");
}
} }
} };
};
return ( return (
<div key={item.label} className="relative"> <div key={item.label} className="relative">
{item.label === t("publication") ? ( {item.label === t("publication") ? (
<> <>
<button <button
onClick={() => setDropdownOpen(!isDropdownOpen)} onClick={() => setDropdownOpen(!isDropdownOpen)}
className={cn(
"relative text-gray-500 dark:text-white dark:hover:text-slate-300 hover:text-black transition-colors",
isDropdownOpen ||
pathname.startsWith("/public/publication")
? "text-black"
: "",
)}
>
{item.label}
<span
className={cn(
"absolute -bottom-1 left-1/2 -translate-x-1/2 w-6 h-[3px] bg-red-800 rounded transition-all",
isDropdownOpen ||
pathname.startsWith("/public/publication")
? "opacity-100"
: "opacity-0",
)}
/>
</button>
{isDropdownOpen && (
<div className="absolute top-full mt-2 w-48 bg-white border rounded shadow z-50">
{PUBLIKASI_SUBMENU.map((sub) => (
<Link
key={sub.label}
href={sub.href}
className="block px-4 py-2 text-sm hover:bg-gray-100 text-gray-700 dark:text-white"
>
{sub.label}
</Link>
))}
</div>
)}
</>
) : (
<Link
href={item.href}
onClick={handleClick}
className={cn( className={cn(
"relative text-gray-500 dark:text-white dark:hover:text-slate-300 hover:text-black transition-colors", "relative text-gray-500 dark:text-white dark:hover:text-slate-300 hover:text-black transition-colors",
isDropdownOpen || isActive && "text-black",
pathname.startsWith("/public/publication")
? "text-black"
: "",
)} )}
> >
{item.label} {item.label}
<span {isActive && (
className={cn( <span className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-6 h-[3px] bg-red-800 rounded" />
"absolute -bottom-1 left-1/2 -translate-x-1/2 w-6 h-[3px] bg-red-800 rounded transition-all", )}
isDropdownOpen || </Link>
pathname.startsWith("/public/publication") )}
? "opacity-100"
: "opacity-0",
)}
/>
</button>
{isDropdownOpen && (
<div className="absolute top-full mt-2 w-48 bg-white border rounded shadow z-50">
{PUBLIKASI_SUBMENU.map((sub) => (
<Link
key={sub.label}
href={sub.href}
className="block px-4 py-2 text-sm hover:bg-gray-100 text-gray-700 dark:text-white"
>
{sub.label}
</Link>
))}
</div>
)}
</>
) : (
<Link
href={item.href}
onClick={handleClick}
className={cn(
"relative text-gray-500 dark:text-white dark:hover:text-slate-300 hover:text-black transition-colors",
isActive && "text-black",
)}
>
{item.label}
{isActive && (
<span className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-6 h-[3px] bg-red-800 rounded" />
)}
</Link>
)}
</div>
);
})}
</nav>
{/* 🔹 PROFILE / LOGIN SECTION */}
<nav className="hidden md:flex items-center gap-3 z-10 relative">
{!isLoggedIn ? (
<>
<Link href="/auth/register">
<Button className="bg-transparent border text-black hover:bg-red-600 hover:text-white cursor-pointer">
{t("register")}{" "}
</Button>
</Link>
<Link href="/auth">
<Button className="bg-red-700 text-white cursor-pointer hover:bg-white hover:border hover:border-red-700 hover:text-red-700">
{t("login")}
</Button>
</Link>
</>
) : (
<div className="relative">
<button
onClick={() => setShowProfileMenu((prev) => !prev)}
className="flex items-center gap-2 border-2 py-1 px-3 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-700 cursor-pointer"
>
<div className="w-9 h-9 rounded-full overflow-hidden border">
<Image
src="/avatar-profile.png"
alt={username || "User avatar"}
width={36}
height={36}
className="object-cover"
/>
</div> </div>
<span className="text-sm font-medium text-gray-800 dark:text-white"> );
{fullname} })}
</span> </nav>
<ChevronDown className="w-4 h-4 text-gray-600" />
</button>
{showProfileMenu && ( {/* 🔹 PROFILE / LOGIN SECTION */}
<div className="absolute right-0 mt-2 w-40 bg-white dark:bg-black border rounded shadow z-50 "> <nav className="hidden md:flex items-center gap-3 z-10 relative">
<Link {!isLoggedIn ? (
href="/admin/dashboard" <>
className="flex flex-row items-center text-left px-4 py-2 hover:bg-gray-300 dark:hover:bg-gray-700 text-gray-700 dark:text-white" <Link href="/auth/register">
> <Button className="bg-transparent border text-black hover:bg-red-600 hover:text-white cursor-pointer">
<svg {t("register")}{" "}
xmlns="http://www.w3.org/2000/svg" </Button>
width="20" </Link>
height="20" <Link href="/auth">
viewBox="0 0 24 24" <Button className="bg-red-700 text-white cursor-pointer hover:bg-white hover:border hover:border-red-700 hover:text-red-700">
> {t("login")}
<path </Button>
fill="currentColor" </Link>
d="M14 21a1 1 0 0 1-1-1v-8a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1zM4 13a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1zm5-2V5H5v6zM4 21a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1zm1-2h4v-2H5zm10 0h4v-6h-4zM13 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1zm2 1v2h4V5z" </>
/> ) : (
</svg> <div className="relative">
&nbsp;Dashboard <button
</Link> onClick={() => setShowProfileMenu((prev) => !prev)}
className="flex items-center gap-2 border-2 py-1 px-3 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-700 cursor-pointer"
>
<div className="w-9 h-9 rounded-full overflow-hidden border">
<Image
src="/avatar-profile.png"
alt={username || "User avatar"}
width={36}
height={36}
className="object-cover"
/>
</div>
<span className="text-sm font-medium text-gray-800 dark:text-white">
{fullname}
</span>
<ChevronDown className="w-4 h-4 text-gray-600" />
</button>
<button {showProfileMenu && (
onClick={handleLogout} <div className="absolute right-0 mt-2 w-40 bg-white dark:bg-black border rounded shadow z-50 ">
className="w-full flex flex-row text-left px-4 py-2 hover:bg-gray-300 text-gray-700 dark:text-white cursor-pointer" <Link
> href="/admin/dashboard"
<svg className="flex flex-row items-center text-left px-4 py-2 hover:bg-gray-300 dark:hover:bg-gray-700 text-gray-700 dark:text-white"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
> >
<g <svg
fill="none" xmlns="http://www.w3.org/2000/svg"
stroke="currentColor" width="20"
strokeLinecap="round" height="20"
strokeWidth="1.5" viewBox="0 0 24 24"
> >
<path <path
strokeLinejoin="round" fill="currentColor"
d="M13.477 21.245H8.34a4.92 4.92 0 0 1-5.136-4.623V7.378A4.92 4.92 0 0 1 8.34 2.755h5.136" d="M14 21a1 1 0 0 1-1-1v-8a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1zM4 13a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1zm5-2V5H5v6zM4 21a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1zm1-2h4v-2H5zm10 0h4v-6h-4zM13 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1zm2 1v2h4V5z"
/> />
<path strokeMiterlimit="10" d="M20.795 12H7.442" /> </svg>
<path &nbsp;Dashboard
strokeLinejoin="round" </Link>
d="m16.083 17.136l4.404-4.404a1.04 1.04 0 0 0 0-1.464l-4.404-4.404"
/>
</g>
</svg>
&nbsp;{t("logout")}
</button>
</div>
)}
</div>
)}
</nav>
{/* 📱 SIDEBAR MOBILE */} <button
{isSidebarOpen && ( onClick={handleLogout}
<div className="fixed inset-0 z-50 flex"> className="w-full flex flex-row text-left px-4 py-2 hover:bg-gray-300 text-gray-700 dark:text-white cursor-pointer"
<div className="w-80 bg-white p-6 space-y-6 shadow-lg relative h-full overflow-y-auto"> >
<button <svg
onClick={() => setIsSidebarOpen(false)} xmlns="http://www.w3.org/2000/svg"
className="absolute top-4 right-4 text-gray-600" width="20"
> height="20"
viewBox="0 0 24 24"
</button> >
<g
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeWidth="1.5"
>
<path
strokeLinejoin="round"
d="M13.477 21.245H8.34a4.92 4.92 0 0 1-5.136-4.623V7.378A4.92 4.92 0 0 1 8.34 2.755h5.136"
/>
<path strokeMiterlimit="10" d="M20.795 12H7.442" />
<path
strokeLinejoin="round"
d="m16.083 17.136l4.404-4.404a1.04 1.04 0 0 0 0-1.464l-4.404-4.404"
/>
</g>
</svg>
&nbsp;{t("logout")}
</button>
</div>
)}
</div>
)}
</nav>
<div className="mt-10"> {/* 📱 SIDEBAR MOBILE */}
<h3 className="text-[16px] font-bold text-gray-700 mb-2"> {isSidebarOpen && (
{t("language")} <div className="fixed inset-0 z-50 flex">
</h3> <div className="w-80 bg-white p-6 space-y-6 shadow-lg relative h-full overflow-y-auto">
{/* button language */} <button
<div className={`relative text-left border w-fit rounded-lg`}> onClick={() => setIsSidebarOpen(false)}
<LocalSwitcher /> className="absolute top-4 right-4 text-gray-600"
</div> >
{/* <div className="space-y-5 ml-3">
</button>
<div className="mt-10">
<h3 className="text-[16px] font-bold text-gray-700 mb-2">
{t("language")}
</h3>
{/* button language */}
<div className={`relative text-left border w-fit rounded-lg`}>
<LocalSwitcher />
</div>
{/* <div className="space-y-5 ml-3">
<button className="flex items-center gap-2 text-sm text-gray-800"> <button className="flex items-center gap-2 text-sm text-gray-800">
<Image src={"/Flag.svg"} width={24} height={24} alt="flag" /> <Image src={"/Flag.svg"} width={24} height={24} alt="flag" />
English English
@ -309,86 +311,99 @@ export default function Navbar() {
Bahasa Indonesia Bahasa Indonesia
</button> </button>
</div> */} </div> */}
</div>
<div>
<h3 className="text-[16px] font-bold text-gray-700 dark:text-white mb-2">
{t("features")}
</h3>
<div className="space-y-5 ml-3">
{NAV_ITEMS.map((item) => (
<button
key={item.label}
onClick={() => {
if (item.label === t("forYou")) {
if (!checkLoginStatus()) {
router.push("/auth");
} else {
router.push("/for-you");
}
} else {
router.push(item.href);
}
setIsSidebarOpen(false);
}}
className="block text-[15px] text-gray-800 dark:text-white text-left w-full"
>
{item.label}
</button>
))}
</div> </div>
</div>
<div className="space-y-5 text-[16px] font-bold"> <div>
<Link href="/about" className="block text-black dark:text-white"> <h3 className="text-[16px] font-bold text-gray-700 dark:text-white mb-2">
{t("about")} {t("features")}
</Link> </h3>
<Link href="/advertising" className="block text-black dark:text-white"> <div className="space-y-5 ml-3">
{t("advertising")} {NAV_ITEMS.map((item) => (
</Link> <button
<Link href="/contact" className="block text-black dark:text-white"> key={item.label}
{t("contact")} onClick={() => {
</Link> if (item.label === t("forYou")) {
if (!checkLoginStatus()) {
router.push("/auth");
} else {
router.push("/for-you");
}
} else {
router.push(item.href);
}
setIsSidebarOpen(false);
}}
className="block text-[15px] text-gray-800 dark:text-white text-left w-full"
>
{item.label}
</button>
))}
</div>
</div>
{!isLoggedIn ? ( <div className="space-y-5 text-[16px] font-bold">
<> <Link
<Link href="/auth" className="block text-lg text-gray-800 dark:text-white"> href="/about"
{t("login")} className="block text-black dark:text-white"
</Link>
<Link
href="/auth/register"
className="block text-lg text-gray-800 dark:text-white"
>
{t("register")}
</Link>
</>
) : (
<button
onClick={handleLogout}
className="block text-left w-full text-lg text-red-600 hover:underline"
> >
{t("logout")} {t("about")}
</button> </Link>
)} <Link
href="/advertising"
className="block text-black dark:text-white"
>
{t("advertising")}
</Link>
<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"
>
{t("login")}
</Link>
<Link
href="/auth/register"
className="block text-lg text-gray-800 dark:text-white"
>
{t("register")}
</Link>
</>
) : (
<button
onClick={handleLogout}
className="block text-left w-full text-lg text-red-600 hover:underline"
>
{t("logout")}
</button>
)}
</div>
<Card className="rounded-none p-4">
<h2 className="text-[#C6A455] text-center text-lg font-semibold mb-2">
{t("subscribeTitle")}
</h2>
<Input type="email" placeholder={t("subscribePlaceholder")} />
<Button className="bg-[#C6A455] mt-2">
{t("subscribeButton")}
</Button>
</Card>
</div> </div>
<Card className="rounded-none p-4"> <div
<h2 className="text-[#C6A455] text-center text-lg font-semibold mb-2"> className="flex-1 bg-black/50"
{t("subscribeTitle")} onClick={() => setIsSidebarOpen(false)}
</h2> />
<Input type="email" placeholder={t("subscribePlaceholder")} />
<Button className="bg-[#C6A455] mt-2">
{t("subscribeButton")}
</Button>
</Card>
</div> </div>
)}
<div </header>
className="flex-1 bg-black/50" </RevealT>
onClick={() => setIsSidebarOpen(false)}
/>
</div>
)}
</header>
); );
} }

View File

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

58
components/ui/Reveal.tsx Normal file
View File

@ -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 (
<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>
);
};

63
components/ui/RevealL.tsx Normal file
View File

@ -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 (
<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>
);
};

63
components/ui/RevealR.tsx Normal file
View File

@ -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 (
<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>
);
};

63
components/ui/RevealT.tsx Normal file
View File

@ -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 (
<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>
);
};