fix: add reveal animation in landing
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
This commit is contained in:
parent
a71cbeefeb
commit
24b44a491a
|
|
@ -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,6 +56,7 @@ 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">
|
||||
|
|
@ -93,7 +95,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
|
||||
}}
|
||||
|
|
@ -106,5 +108,6 @@ export default function Category() {
|
|||
)}
|
||||
</div>
|
||||
</section>
|
||||
</RevealR>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,6 +101,7 @@ 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">
|
||||
|
|
@ -273,5 +275,6 @@ export default function Footer() {
|
|||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</Reveal>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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) {
|
||||
|
|
@ -128,12 +131,13 @@ export default function Header() {
|
|||
if (bookmarkedIds.size > 0) {
|
||||
localStorage.setItem(
|
||||
"bookmarkedIds",
|
||||
JSON.stringify(Array.from(bookmarkedIds))
|
||||
JSON.stringify(Array.from(bookmarkedIds)),
|
||||
);
|
||||
}
|
||||
}, [bookmarkedIds]);
|
||||
|
||||
return (
|
||||
<RevealR>
|
||||
<section className="max-w-[1350px] mx-auto px-4">
|
||||
<div className="flex flex-col lg:flex-row gap-6 py-6">
|
||||
{data.length > 0 && (
|
||||
|
|
@ -197,6 +201,7 @@ export default function Header() {
|
|||
</Swiper>
|
||||
</div>
|
||||
</section>
|
||||
</RevealR>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -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 (
|
||||
<div>
|
||||
<RevealL>
|
||||
<div
|
||||
className={`rounded-xl overflow-hidden shadow hover:shadow-lg transition-all bg-white dark:bg-black dark:border dark:border-slate-50 ${
|
||||
isBig
|
||||
|
|
@ -312,12 +331,10 @@ function Card({
|
|||
fill
|
||||
className="object-cover"
|
||||
/> */}
|
||||
<Image
|
||||
<ImageBlurry
|
||||
src={imageSrc}
|
||||
alt={item.title}
|
||||
fill
|
||||
className="object-cover"
|
||||
onError={() => setImageSrc(DEFAULT_IMAGE)}
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
|
@ -379,7 +396,7 @@ function Card({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</RevealL>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,24 +342,37 @@ 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 (
|
||||
<Image
|
||||
<ImageBlurry
|
||||
src={imgSrc}
|
||||
alt={alt || "Image"}
|
||||
fill
|
||||
className="object-cover cursor-pointer hover:opacity-90 transition-opacity"
|
||||
onError={() => setImgSrc(DEFAULT_IMAGE)}
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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">
|
||||
|
|
@ -580,5 +595,6 @@ export default function MediaUpdate() {
|
|||
)}
|
||||
</div>
|
||||
</section>
|
||||
</RevealL>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,6 +76,7 @@ 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
|
||||
|
|
@ -340,19 +342,31 @@ 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
|
||||
|
|
@ -390,5 +404,6 @@ export default function Navbar() {
|
|||
</div>
|
||||
)}
|
||||
</header>
|
||||
</RevealT>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }) {
|
|||
<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.viewCount || 0}
|
||||
{data.clickCount || 0}{" "}
|
||||
</span>
|
||||
<span className="text-black">
|
||||
Creator: {data.creatorGroupLevelName}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
Loading…
Reference in New Issue