"use client"
import * as React from "react"
import { footerStyles } from "./footer-styles"
import { DiscordIcon, FacebookIcon, GitHubIcon, XIcon } from "./icons"
import { NewsletterForm } from "./newsletter-form"
import {
type ContactDetailItem,
type LetterRevealFooterProps,
type NavColumnItem,
type SocialLinkItem,
} from "./types"
const DEFAULT_SOCIAL_LINKS: SocialLinkItem[] = [
{ platform: "x", href: "#", label: "X" },
{ platform: "facebook", href: "#", label: "Facebook" },
{ platform: "github", href: "#", label: "GitHub" },
{ platform: "discord", href: "#", label: "Discord" },
]
const DEFAULT_CONTACT_DETAILS: ContactDetailItem[] = [
{
label: "Headquarters",
value: "2210 Larkin Hollow, Portland,\nOR 97205",
hideOnMobile: true,
},
{
label: "Call us",
value: "+1 (503) 555-0142",
href: "tel:+15035550142",
isLink: true,
underlineOnHover: false,
},
{
label: "Contact us",
value: "hello@jolocky.org",
href: "mailto:hello@jolocky.org",
isLink: true,
underlineOnHover: true,
},
{
label: "Support hours",
value: "Mon – Fri, 09.00am – 06.00pm",
hideOnMobile: true,
},
]
const DEFAULT_NAV_COLUMNS: NavColumnItem[] = [
{
title: "Navigation",
links: [
{ label: "Home", href: "#" },
{ label: "Curriculum", href: "#" },
{ label: "About", href: "#" },
{ label: "Contact", href: "#" },
],
},
{
title: "Resources",
links: [
{ label: "Blog", href: "#" },
{ label: "FAQ", href: "#" },
{ label: "Forum", href: "#" },
],
},
]
function renderMaskedText(text: string) {
let charIndex = 0
const lines = text.split("|")
return lines.map((line, lineIdx) => {
const words = line.split(" ")
return (
<span key={lineIdx} className="jf__line" aria-hidden="true">
{words.map((word, wordIdx) => {
const chars = Array.from(word)
return (
<span key={wordIdx} className="jf__word">
{chars.map((char) => {
const currentIndex = charIndex++
return (
<span
key={currentIndex}
className="jf__char"
style={{ "--i": currentIndex } as React.CSSProperties}
>
{char}
</span>
)
})}
</span>
)
})}
</span>
)
})
}
const subscribeNever = () => () => {}
export function LetterRevealFooter({
className = "",
headlineDesktop = "Start coding with us,|for free.",
headlineMobile = "Start coding|with us.",
socialLinks = DEFAULT_SOCIAL_LINKS,
contactDetails = DEFAULT_CONTACT_DETAILS,
navColumns = DEFAULT_NAV_COLUMNS,
supportHeadline = "Support Jolocky",
supportDescription = "Jolocky is free and nonprofit. Help keep it that way.",
donateHref = "#",
newsletterPlaceholder = "Enter your email",
copyrightText = "© Copyright 2026 Jolocky. All rights reserved",
termsHref = "#",
privacyHref = "#",
onNewsletterSubmit,
}: LetterRevealFooterProps) {
const footerRef = React.useRef<HTMLElement | null>(null)
const [hasEntered, setHasEntered] = React.useState(false)
// Without IntersectionObserver the headline is simply shown right away.
const canObserve = React.useSyncExternalStore(
subscribeNever,
() => "IntersectionObserver" in window,
() => true
)
const isRevealed = hasEntered || !canObserve
// IntersectionObserver to trigger character slide-up reveal
React.useEffect(() => {
const footer = footerRef.current
if (!footer || !canObserve) return
const observer = new IntersectionObserver(
(entries) => {
if (entries.some((e) => e.isIntersecting)) {
setHasEntered(true)
observer.disconnect()
}
},
{ threshold: 0.25 }
)
observer.observe(footer)
return () => observer.disconnect()
}, [canObserve])
const renderSocialIcon = (
platform: "x" | "facebook" | "github" | "discord"
) => {
switch (platform) {
case "x":
return <XIcon size={17} />
case "facebook":
return <FacebookIcon size={17} />
case "github":
return <GitHubIcon size={17} />
case "discord":
return <DiscordIcon size={17} />
}
}
const rawDesktopText = headlineDesktop.replace(/\|/g, " ")
const rawMobileText = headlineMobile.replace(/\|/g, " ")
return (
<footer
ref={footerRef}
className={`jf${isRevealed ? "is-revealed" : ""}${className ? ` ${className}` : ""}`}
>
<style>{footerStyles}</style>
<div className="jf__inner">
{/* Top Section */}
<div className="jf__top">
<div>
{/* Desktop Headline */}
<h2
className="jf__title jf__desktop-only"
aria-label={rawDesktopText}
>
{renderMaskedText(headlineDesktop)}
</h2>
{/* Mobile Headline */}
<h2
className="jf__title jf__mobile-only"
aria-label={rawMobileText}
>
{renderMaskedText(headlineMobile)}
</h2>
{/* Social Buttons */}
<div className="jf__socials">
{socialLinks.map((item, idx) => (
<a
key={idx}
className="jf__social"
href={item.href}
aria-label={item.label}
title={item.label}
>
{renderSocialIcon(item.platform)}
</a>
))}
</div>
</div>
{/* Contact Info Grid */}
<div className="jf__info">
{contactDetails.map((item, idx) => (
<div
key={idx}
className={item.hideOnMobile ? "jf__hide-mobile" : undefined}
>
<div className="jf__label">{item.label}</div>
{item.isLink && item.href ? (
<a
className={`jf__value${item.underlineOnHover ? "jf__link" : ""}`}
href={item.href}
>
{item.value}
</a>
) : (
<div className="jf__value">
{typeof item.value === "string" && item.value.includes("\n")
? item.value.split("\n").map((line, i, arr) => (
<React.Fragment key={i}>
{line}
{i < arr.length - 1 && <br />}
</React.Fragment>
))
: item.value}
</div>
)}
</div>
))}
</div>
</div>
<div className="jf__divider jf__divider--top" />
{/* Columns Section */}
<div className="jf__cols">
{navColumns.map((col, idx) =>
idx === 0 ? (
<nav key={idx} aria-label="Footer navigation">
<div className="jf__label">{col.title}</div>
<div className="jf__list">
{col.links.map((link, linkIdx) => (
<a key={linkIdx} className="jf__link" href={link.href}>
{link.label}
</a>
))}
</div>
</nav>
) : (
<div key={idx}>
<div className="jf__label">{col.title}</div>
<div className="jf__list">
{col.links.map((link, linkIdx) => (
<a key={linkIdx} className="jf__link" href={link.href}>
{link.label}
</a>
))}
</div>
</div>
)
)}
{/* Support Block with Inverse Underline */}
<div className="jf__support">
<div>
<div className="jf__label">{supportHeadline}</div>
<p>{supportDescription}</p>
<a className="jf__link jf__link--on jf__donate" href={donateHref}>
Donate
</a>
</div>
<a className="jf__donate-btn" href={donateHref}>
Donate
</a>
</div>
{/* Subscribe Pill Form */}
<NewsletterForm
placeholder={newsletterPlaceholder}
onSubmit={onNewsletterSubmit}
/>
</div>
<div className="jf__divider" />
{/* Bottom Bar */}
<div className="jf__bottom">
<div className="jf__legal">
<a className="jf__link" href={termsHref}>
Terms of Service
</a>
<span className="jf__copy jf__desktop-only">{copyrightText}</span>
<a className="jf__link" href={privacyHref}>
Privacy Policy
</a>
</div>
<span className="jf__copy jf__mobile-only">{copyrightText}</span>
</div>
</div>
</footer>
)
}