mirror of
https://github.com/lingke-net/koring-space.git
synced 2026-09-12 05:45:17 +08:00
Add CardNav, theme providers, and UI updates
Introduce an interactive top navigation and theming + site UI/branding changes. - Add CardNav component (TSX + CSS) with GSAP animations and responsive behavior. - Dynamic Header now loads CardNav (client-side). - Add Providers (next-themes) and wrap RootLayout; enable theme toggle and update site title to "Koring Team". - Add Koring logo component and public SVG asset. - Add custom 404 (app/not-found.tsx). - Update home page spacing and card paddings for responsiveness. - Refactor Footer content and layout for legal/branding text. - Add dependencies: gsap, react-icons and update pnpm lock.
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
"use client";
|
||||
|
||||
import { useLayoutEffect, useRef, useState } from "react";
|
||||
import { gsap } from "gsap";
|
||||
import { GoArrowUpRight } from "react-icons/go";
|
||||
import { useTheme } from "next-themes";
|
||||
import "./CardNav.css";
|
||||
|
||||
interface CardNavLink {
|
||||
label: string;
|
||||
href?: string;
|
||||
ariaLabel: string;
|
||||
}
|
||||
|
||||
interface CardNavItem {
|
||||
label: string;
|
||||
bgColor: string;
|
||||
textColor: string;
|
||||
links: CardNavLink[];
|
||||
}
|
||||
|
||||
interface CardNavProps {
|
||||
logo?: string;
|
||||
logoAlt?: string;
|
||||
items: CardNavItem[];
|
||||
className?: string;
|
||||
ease?: string;
|
||||
baseColor?: string;
|
||||
menuColor?: string;
|
||||
buttonBgColor?: string;
|
||||
buttonTextColor?: string;
|
||||
}
|
||||
|
||||
const CardNav: React.FC<CardNavProps> = ({
|
||||
logo,
|
||||
logoAlt = "Logo",
|
||||
items,
|
||||
className = "",
|
||||
ease = "power3.out",
|
||||
baseColor = "#fff",
|
||||
menuColor,
|
||||
buttonBgColor,
|
||||
buttonTextColor,
|
||||
}) => {
|
||||
const [isHamburgerOpen, setIsHamburgerOpen] = useState(false);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const navRef = useRef<HTMLElement>(null);
|
||||
const cardsRef = useRef<HTMLDivElement[]>([]);
|
||||
const tlRef = useRef<gsap.core.Timeline | null>(null);
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
const calculateHeight = () => {
|
||||
const navEl = navRef.current;
|
||||
if (!navEl) return 260;
|
||||
|
||||
const isMobile = window.matchMedia("(max-width: 768px)").matches;
|
||||
if (isMobile) {
|
||||
const contentEl = navEl.querySelector<HTMLElement>(".card-nav-content");
|
||||
if (contentEl) {
|
||||
const wasVisible = contentEl.style.visibility;
|
||||
const wasPointerEvents = contentEl.style.pointerEvents;
|
||||
const wasPosition = contentEl.style.position;
|
||||
const wasHeight = contentEl.style.height;
|
||||
|
||||
contentEl.style.visibility = "visible";
|
||||
contentEl.style.pointerEvents = "auto";
|
||||
contentEl.style.position = "static";
|
||||
contentEl.style.height = "auto";
|
||||
|
||||
contentEl.offsetHeight;
|
||||
|
||||
const topBar = 60;
|
||||
const padding = 16;
|
||||
const contentHeight = contentEl.scrollHeight;
|
||||
|
||||
contentEl.style.visibility = wasVisible;
|
||||
contentEl.style.pointerEvents = wasPointerEvents;
|
||||
contentEl.style.position = wasPosition;
|
||||
contentEl.style.height = wasHeight;
|
||||
|
||||
return topBar + contentHeight + padding;
|
||||
}
|
||||
}
|
||||
return 260;
|
||||
};
|
||||
|
||||
const createTimeline = () => {
|
||||
const navEl = navRef.current;
|
||||
if (!navEl) return null;
|
||||
|
||||
gsap.set(navEl, { height: 60 });
|
||||
gsap.set(cardsRef.current, { y: 50, opacity: 0 });
|
||||
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
tl.to(navEl, {
|
||||
height: calculateHeight,
|
||||
duration: 0.4,
|
||||
ease,
|
||||
});
|
||||
|
||||
tl.to(
|
||||
cardsRef.current,
|
||||
{ y: 0, opacity: 1, duration: 0.4, ease, stagger: 0.08 },
|
||||
"-=0.1"
|
||||
);
|
||||
|
||||
return tl;
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const tl = createTimeline();
|
||||
tlRef.current = tl;
|
||||
|
||||
return () => {
|
||||
tl?.kill();
|
||||
tlRef.current = null;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ease, items]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (!tlRef.current) return;
|
||||
|
||||
if (isExpanded) {
|
||||
const newHeight = calculateHeight();
|
||||
gsap.set(navRef.current, { height: newHeight });
|
||||
|
||||
tlRef.current.kill();
|
||||
const newTl = createTimeline();
|
||||
if (newTl) {
|
||||
newTl.progress(1);
|
||||
tlRef.current = newTl;
|
||||
}
|
||||
} else {
|
||||
tlRef.current.kill();
|
||||
const newTl = createTimeline();
|
||||
if (newTl) {
|
||||
tlRef.current = newTl;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isExpanded]);
|
||||
|
||||
const toggleMenu = () => {
|
||||
const tl = tlRef.current;
|
||||
if (!tl) return;
|
||||
if (!isExpanded) {
|
||||
setIsHamburgerOpen(true);
|
||||
setIsExpanded(true);
|
||||
tl.play(0);
|
||||
} else {
|
||||
setIsHamburgerOpen(false);
|
||||
tl.eventCallback("onReverseComplete", () => setIsExpanded(false));
|
||||
tl.reverse();
|
||||
}
|
||||
};
|
||||
|
||||
const setCardRef =
|
||||
(i: number) =>
|
||||
(el: HTMLDivElement | null) => {
|
||||
if (el) cardsRef.current[i] = el;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`card-nav-container ${className}`}>
|
||||
<nav
|
||||
ref={navRef}
|
||||
className={`card-nav ${isExpanded ? "open" : ""}`}
|
||||
style={{ backdropFilter: "blur(20px) saturate(1.4)", WebkitBackdropFilter: "blur(20px) saturate(1.4)" }}
|
||||
>
|
||||
<div className="card-nav-top">
|
||||
<div className="card-nav-left">
|
||||
<div
|
||||
className={`hamburger-menu ${isHamburgerOpen ? "open" : ""}`}
|
||||
onClick={toggleMenu}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
toggleMenu();
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
aria-label={isExpanded ? "Close menu" : "Open menu"}
|
||||
aria-expanded={isExpanded}
|
||||
tabIndex={0}
|
||||
style={{ color: menuColor || "#000" }}
|
||||
>
|
||||
<div className="hamburger-line" />
|
||||
<div className="hamburger-line" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="logo-container">
|
||||
<a href="/" className="no-underline">
|
||||
{logo ? (
|
||||
<img src={logo} alt={logoAlt} className="logo" />
|
||||
) : (
|
||||
<span className="logo-text" style={{ cursor: "pointer" }}>{logoAlt}</span>
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="theme-toggle"
|
||||
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
||||
aria-label="Toggle dark mode"
|
||||
>
|
||||
{theme === "dark" ? (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<path d="M12 2v2" />
|
||||
<path d="M12 20v2" />
|
||||
<path d="m4.93 4.93 1.41 1.41" />
|
||||
<path d="m17.66 17.66 1.41 1.41" />
|
||||
<path d="M2 12h2" />
|
||||
<path d="M20 12h2" />
|
||||
<path d="m6.34 17.66-1.41 1.41" />
|
||||
<path d="m19.07 4.93-1.41 1.41" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card-nav-content" aria-hidden={!isExpanded}>
|
||||
{(items || []).slice(0, 3).map((item, idx) => (
|
||||
<div
|
||||
key={`${item.label}-${idx}`}
|
||||
className="nav-card"
|
||||
ref={setCardRef(idx)}
|
||||
style={{ backgroundColor: item.bgColor, color: item.textColor }}
|
||||
>
|
||||
<div className="nav-card-label">{item.label}</div>
|
||||
<div className="nav-card-links">
|
||||
{item.links?.map((lnk, i) => (
|
||||
<a
|
||||
key={`${lnk.label}-${i}`}
|
||||
className="nav-card-link"
|
||||
href={lnk.href}
|
||||
aria-label={lnk.ariaLabel}
|
||||
>
|
||||
<GoArrowUpRight className="nav-card-link-icon" aria-hidden="true" />
|
||||
{lnk.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardNav;
|
||||
Reference in New Issue
Block a user