- Remove unsupported next: { revalidate } option from API routes
- Add Cloudflare-specific cache configuration (cf.cacheTtl)
- Add User-Agent and Referer headers to bypass Plan API 403 errors
- Enable workers_dev and preview_urls in wrangler.jsonc to prevent disabling on deployment
278 lines
9.4 KiB
TypeScript
278 lines
9.4 KiB
TypeScript
"use client"
|
|
import { motion } from "framer-motion"
|
|
|
|
// Animation variants for reusability
|
|
const containerVariants = {
|
|
hidden: { opacity: 0, y: 50 },
|
|
visible: {
|
|
opacity: 1,
|
|
y: 0,
|
|
transition: {
|
|
duration: 0.8,
|
|
staggerChildren: 0.1,
|
|
},
|
|
},
|
|
}
|
|
|
|
const itemVariants = {
|
|
hidden: { opacity: 0, x: -20 },
|
|
visible: {
|
|
opacity: 1,
|
|
x: 0,
|
|
transition: { duration: 0.6 },
|
|
},
|
|
}
|
|
|
|
const linkVariants = {
|
|
hidden: { opacity: 0, y: 10 },
|
|
visible: {
|
|
opacity: 1,
|
|
y: 0,
|
|
transition: { duration: 0.4 },
|
|
},
|
|
}
|
|
|
|
const socialVariants = {
|
|
hidden: { opacity: 0, scale: 0 },
|
|
visible: {
|
|
opacity: 1,
|
|
scale: 1,
|
|
transition: {
|
|
type: "spring" as const,
|
|
stiffness: 200,
|
|
damping: 10,
|
|
},
|
|
},
|
|
}
|
|
|
|
const backgroundVariants = {
|
|
hidden: { opacity: 0, scale: 0.8 },
|
|
visible: {
|
|
opacity: 1,
|
|
scale: 1,
|
|
transition: {
|
|
duration: 2,
|
|
},
|
|
},
|
|
}
|
|
|
|
// Footer data for better maintainability
|
|
const footerData = {
|
|
sections: [
|
|
{ title: "About", links: ["Home", "Projects", "Our Mission", "Contact Us"] },
|
|
{ title: "Education", links: ["News", "Learn", "Certification", "Publications"] },
|
|
{ title: "Services", links: ["Web Design", "Development", "Consulting", "Support"] },
|
|
{ title: "Resources", links: ["Blog", "Documentation", "Community", "Help Center"] },
|
|
],
|
|
social: [
|
|
{ href: "#", label: "Twitter", icon: "T" },
|
|
{ href: "#", label: "GitHub", icon: "G" },
|
|
{ href: "#", label: "LinkedIn", icon: "L" },
|
|
],
|
|
title: "Sticky Footer",
|
|
subtitle: "Scroll-triggered design",
|
|
copyright: "©2024 All rights reserved",
|
|
}
|
|
|
|
// Reusable components
|
|
const NavSection = ({ title, links, index }: { title: string; links: string[]; index: number }) => (
|
|
<motion.div variants={itemVariants} custom={index} className="flex flex-col gap-2">
|
|
<motion.h3
|
|
initial={{ opacity: 0, y: -10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.3 + index * 0.1, duration: 0.5 }}
|
|
className="mb-2 uppercase text-muted-foreground text-xs font-semibold tracking-wider border-b border-border pb-1 hover:text-foreground transition-colors duration-300"
|
|
>
|
|
{title}
|
|
</motion.h3>
|
|
{links.map((link, linkIndex) => (
|
|
<motion.a
|
|
key={linkIndex}
|
|
variants={linkVariants}
|
|
custom={linkIndex}
|
|
href="#"
|
|
whileHover={{
|
|
x: 8,
|
|
transition: { type: "spring", stiffness: 300, damping: 20 },
|
|
}}
|
|
className="text-muted-foreground hover:text-foreground transition-colors duration-300 font-sans text-xs md:text-sm group relative"
|
|
>
|
|
<span className="relative">
|
|
{link}
|
|
<motion.span
|
|
className="absolute bottom-0 left-0 h-0.5 bg-primary"
|
|
initial={{ width: 0 }}
|
|
whileHover={{ width: "100%" }}
|
|
transition={{ duration: 0.3 }}
|
|
/>
|
|
</span>
|
|
</motion.a>
|
|
))}
|
|
</motion.div>
|
|
)
|
|
|
|
const SocialLink = ({ href, label, icon, index }: { href: string; label: string; icon: string; index: number }) => (
|
|
<motion.a
|
|
variants={socialVariants}
|
|
custom={index}
|
|
href={href}
|
|
whileHover={{
|
|
scale: 1.2,
|
|
rotate: 12,
|
|
transition: { type: "spring", stiffness: 300, damping: 15 },
|
|
}}
|
|
whileTap={{ scale: 0.9 }}
|
|
className="w-6 h-6 md:w-8 md:h-8 rounded-full bg-muted hover:bg-gradient-to-r hover:from-primary hover:to-secondary flex items-center justify-center transition-colors duration-300 group"
|
|
aria-label={label}
|
|
>
|
|
<motion.span
|
|
className="text-xs md:text-sm font-bold text-muted-foreground group-hover:text-primary-foreground"
|
|
whileHover={{ scale: 1.1 }}
|
|
>
|
|
{icon}
|
|
</motion.span>
|
|
</motion.a>
|
|
)
|
|
|
|
export default function StickyFooter() {
|
|
return (
|
|
<div className="relative h-[70vh] " style={{ clipPath: "polygon(0% 0, 100% 0%, 100% 100%, 0 100%)" }}>
|
|
<div className="relative h-[calc(100vh+70vh)] -top-[100vh]">
|
|
<div className="h-[70vh] sticky top-[calc(100vh-70vh)]">
|
|
<motion.div
|
|
initial="hidden"
|
|
animate="visible"
|
|
variants={containerVariants}
|
|
className="bg-gradient-to-br from-card via-muted to-card/90 py-6 md:py-12 px-4 md:px-12 h-full w-full flex flex-col justify-between relative overflow-hidden"
|
|
>
|
|
{/* Animated Background Elements */}
|
|
<div className="absolute inset-0 bg-gradient-to-t from-background/20 to-transparent pointer-events-none" />
|
|
|
|
<motion.div
|
|
variants={backgroundVariants}
|
|
className="absolute top-0 right-0 w-48 h-48 md:w-96 md:h-96 bg-primary/5 rounded-full blur-3xl"
|
|
animate={{
|
|
scale: [1, 1.1, 1],
|
|
opacity: [0.3, 0.6, 0.3],
|
|
}}
|
|
transition={{
|
|
duration: 4,
|
|
repeat: Number.POSITIVE_INFINITY,
|
|
ease: "easeInOut",
|
|
}}
|
|
/>
|
|
|
|
<motion.div
|
|
variants={backgroundVariants}
|
|
className="absolute bottom-0 left-0 w-48 h-48 md:w-96 md:h-96 bg-secondary/5 rounded-full blur-3xl"
|
|
animate={{
|
|
scale: [1, 1.2, 1],
|
|
opacity: [0.2, 0.5, 0.2],
|
|
}}
|
|
transition={{
|
|
duration: 5,
|
|
repeat: Number.POSITIVE_INFINITY,
|
|
ease: "easeInOut",
|
|
delay: 1,
|
|
}}
|
|
/>
|
|
|
|
{/* Navigation Section */}
|
|
<motion.div variants={containerVariants} className="relative z-10">
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 md:gap-12 lg:gap-20">
|
|
{footerData.sections.map((section, index) => (
|
|
<NavSection key={section.title} title={section.title} links={section.links} index={index} />
|
|
))}
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* Footer Bottom Section */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 30 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.8, duration: 0.8, ease: "easeOut" }}
|
|
className="flex flex-col md:flex-row justify-between items-start md:items-end relative z-10 gap-4 md:gap-6 mt-6"
|
|
>
|
|
<div className="flex-1">
|
|
<motion.h1
|
|
initial={{ opacity: 0, x: -50 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ delay: 1, duration: 0.8, ease: "easeOut" }}
|
|
whileHover={{
|
|
scale: 1.02,
|
|
transition: { type: "spring", stiffness: 300, damping: 20 },
|
|
}}
|
|
className="text-[12vw] md:text-[10vw] lg:text-[8vw] xl:text-[6vw] leading-[0.8] font-serif bg-gradient-to-r from-foreground via-muted-foreground to-foreground/60 bg-clip-text text-transparent cursor-default"
|
|
>
|
|
{footerData.title}
|
|
</motion.h1>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, width: 0 }}
|
|
animate={{ opacity: 1, width: "auto" }}
|
|
transition={{ delay: 1.2, duration: 0.6 }}
|
|
className="flex items-center gap-3 md:gap-4 mt-3 md:mt-4"
|
|
>
|
|
<motion.div
|
|
className="w-8 md:w-12 h-0.5 bg-gradient-to-r from-primary to-secondary"
|
|
animate={{
|
|
scaleX: [1, 1.2, 1],
|
|
}}
|
|
transition={{
|
|
duration: 2,
|
|
repeat: Number.POSITIVE_INFINITY,
|
|
ease: "easeInOut",
|
|
}}
|
|
/>
|
|
<motion.p
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ delay: 1.4, duration: 0.5 }}
|
|
className="text-muted-foreground text-xs md:text-sm font-sans hover:text-foreground transition-colors duration-300"
|
|
>
|
|
{footerData.subtitle}
|
|
</motion.p>
|
|
</motion.div>
|
|
</div>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, x: 50 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ delay: 1.6, duration: 0.6 }}
|
|
className="text-left md:text-right"
|
|
>
|
|
<motion.p
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ delay: 1.8, duration: 0.5 }}
|
|
className="text-muted-foreground text-xs md:text-sm mb-2 md:mb-3 hover:text-foreground transition-colors duration-300"
|
|
>
|
|
{footerData.copyright}
|
|
</motion.p>
|
|
|
|
<motion.div
|
|
variants={containerVariants}
|
|
initial="hidden"
|
|
animate="visible"
|
|
transition={{ delay: 2, staggerChildren: 0.1 }}
|
|
className="flex gap-2 md:gap-3"
|
|
>
|
|
{footerData.social.map((social, index) => (
|
|
<SocialLink
|
|
key={social.label}
|
|
href={social.href}
|
|
label={social.label}
|
|
icon={social.icon}
|
|
index={index}
|
|
/>
|
|
))}
|
|
</motion.div>
|
|
</motion.div>
|
|
</motion.div>
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|