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,
|
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,6 +56,7 @@ export default function Category() {
|
||||||
categories.length > 0 ? categories : fallbackCategories;
|
categories.length > 0 ? categories : fallbackCategories;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<RevealR>
|
||||||
<section className="px-4 py-10">
|
<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">
|
<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">
|
<h2 className="text-xl font-semibold mb-5">
|
||||||
|
|
@ -93,7 +95,7 @@ export default function Category() {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// Navigate to category page or search by category
|
// Navigate to category page or search by category
|
||||||
console.log(
|
console.log(
|
||||||
`Category clicked: ${categoryTitle} (${categorySlug})`
|
`Category clicked: ${categoryTitle} (${categorySlug})`,
|
||||||
);
|
);
|
||||||
// TODO: Implement navigation to category page
|
// TODO: Implement navigation to category page
|
||||||
}}
|
}}
|
||||||
|
|
@ -106,5 +108,6 @@ export default function Category() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</RevealR>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,6 +101,7 @@ export default function Footer() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Reveal>
|
||||||
<footer className="border-t bg-white dark:bg-default-50 text-center">
|
<footer className="border-t bg-white dark:bg-default-50 text-center">
|
||||||
<style jsx>{swiperStyles}</style>
|
<style jsx>{swiperStyles}</style>
|
||||||
<div className="max-w-[1350px] mx-auto">
|
<div className="max-w-[1350px] mx-auto">
|
||||||
|
|
@ -273,5 +275,6 @@ export default function Footer() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
</Reveal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,12 +131,13 @@ 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 (
|
||||||
|
<RevealR>
|
||||||
<section className="max-w-[1350px] mx-auto px-4">
|
<section className="max-w-[1350px] mx-auto px-4">
|
||||||
<div className="flex flex-col lg:flex-row gap-6 py-6">
|
<div className="flex flex-col lg:flex-row gap-6 py-6">
|
||||||
{data.length > 0 && (
|
{data.length > 0 && (
|
||||||
|
|
@ -197,6 +201,7 @@ export default function Header() {
|
||||||
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,24 +342,37 @@ 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 (
|
||||||
|
<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">
|
<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">
|
<div className="max-w-screen-xl mx-auto">
|
||||||
<h2 className="text-2xl font-semibold text-center mb-6">
|
<h2 className="text-2xl font-semibold text-center mb-6">
|
||||||
|
|
@ -580,5 +595,6 @@ export default function MediaUpdate() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</RevealL>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,6 +76,7 @@ export default function Navbar() {
|
||||||
const fullname = Cookies.get("ufne");
|
const fullname = Cookies.get("ufne");
|
||||||
|
|
||||||
return (
|
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">
|
<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">
|
<div className="flex flex-row items-center justify-between space-x-4 z-10">
|
||||||
<Menu
|
<Menu
|
||||||
|
|
@ -340,19 +342,31 @@ export default function Navbar() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-5 text-[16px] font-bold">
|
<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")}
|
{t("about")}
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/advertising" className="block text-black dark:text-white">
|
<Link
|
||||||
|
href="/advertising"
|
||||||
|
className="block text-black dark:text-white"
|
||||||
|
>
|
||||||
{t("advertising")}
|
{t("advertising")}
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/contact" className="block text-black dark:text-white">
|
<Link
|
||||||
|
href="/contact"
|
||||||
|
className="block text-black dark:text-white"
|
||||||
|
>
|
||||||
{t("contact")}
|
{t("contact")}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{!isLoggedIn ? (
|
{!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")}
|
{t("login")}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
|
|
@ -390,5 +404,6 @@ export default function Navbar() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
|
</RevealT>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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