diff --git a/src/components/AppNavbar.tsx b/src/components/AppNavbar.tsx index 9806e49..9f288fb 100644 --- a/src/components/AppNavbar.tsx +++ b/src/components/AppNavbar.tsx @@ -1,7 +1,93 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { LOGO, SEARCH_ICON } from "../utils/constants"; import useBreakpoint from "../utils/useBreakpoint"; +/* ── Inline SVG icons for bottom bar ── */ +const IconHome = ({ active }: { active: boolean }) => ( + + + + +); + +const IconTrending = ({ active }: { active: boolean }) => ( + + + + +); + +const IconSearch = ({ active }: { active: boolean }) => ( + + + + +); + +const IconGrid = ({ active }: { active: boolean }) => ( + + + + + + +); + +const IconMore = ({ active }: { active: boolean }) => ( + + + + + +); + +type BottomTab = "home" | "trending" | "search" | "categories" | "more"; + export default function AppNavbar({ activeHref, navLinks = [{ label: "HOME", href: "/" }], @@ -9,12 +95,28 @@ export default function AppNavbar({ activeHref?: string; navLinks?: { label: string; href: string }[]; }) { - const { isMobile, isTablet } = useBreakpoint(); - const compact = isMobile || isTablet; - const [menuOpen, setMenuOpen] = useState(false); - const [searchOpen, setSearchOpen] = useState(false); + const { isMobile, isTablet, isHydrated } = useBreakpoint(); + const compact = isHydrated && (isMobile || isTablet); + const [activePanel, setActivePanel] = useState< + "search" | "categories" | "more" | null + >(null); const [searchQuery, setSearchQuery] = useState(""); + // Lock body scroll when a panel is open + useEffect(() => { + if (activePanel && compact) { + document.documentElement.style.overflow = "hidden"; + document.body.style.overflow = "hidden"; + } else { + document.documentElement.style.overflow = ""; + document.body.style.overflow = ""; + } + return () => { + document.documentElement.style.overflow = ""; + document.body.style.overflow = ""; + }; + }, [activePanel, compact]); + const handleSearch = (e: React.FormEvent) => { e.preventDefault(); if (searchQuery.trim()) { @@ -29,11 +131,91 @@ export default function AppNavbar({ return false; }; - const [categoriesOpen, setCategoriesOpen] = useState(false); + const togglePanel = (panel: "search" | "categories" | "more") => { + setActivePanel(prev => (prev === panel ? null : panel)); + }; + + const getActiveTab = (): BottomTab | null => { + if (activePanel) + return activePanel === "search" + ? "search" + : activePanel === "categories" + ? "categories" + : "more"; + if (activeHref === "/") return "home"; + if (activeHref === "/posts" || activeHref === "/posts/") return "trending"; + return null; + }; + const currentTab = getActiveTab(); + + const categoryLinks = navLinks.filter(link => link.href !== "/"); + + // Mobile loading overlay — shown before hydration on mobile-width screens + // Uses CSS media query so it never appears on desktop regardless of JS state + if (!isHydrated) { + return ( + <> + {/* Desktop banner + header render normally during SSR */} +
+ Site banner + Site logo +
+ {/* Mobile: slim loading header while JS initializes */} +
+ JourKnows +
+ + ); + } return ( <> - {/* Site Banner */} + {/* Site Banner — desktop only */}
+ + {/* ── Top Header ── */}
- {/* Top bar */}
- {/* Left side / Search */} -
- {compact ? ( - - ) : ( + {/* Desktop: search bar */} + {!compact && ( +
+
+ )} - {/* Logo (Centered Absolute) */} + {/* Mobile: centered logo */} {compact && ( )} - {/* Desktop nav links */} + {/* Desktop: nav links */} {!compact && ( )} - - {/* Hamburger */} - {compact && ( - - )}
- - {/* Search bar */} - {compact && searchOpen && ( -
-
- setSearchQuery(e.target.value)} - className="w-full bg-white/12 border border-white/30 rounded-full py-2 px-4 text-white font-display text-[13px] outline-none placeholder:text-white/60 focus:border-white/60 transition-colors" - /> -
-
- )}
- {/* Mobile slide-in menu */} - {compact && menuOpen && ( + {/* ── Mobile Panels (overlay content between header and bottom bar) ── */} + {compact && activePanel && (
setMenuOpen(false)} - className="fixed inset-0 z-[199] bg-black/45" + className="fixed inset-0 z-[198] bg-black/50" + style={{ top: 50, bottom: 64 }} + onClick={() => setActivePanel(null)} > - + {/* Search panel */} + {activePanel === "search" && ( +
+

+ Search Articles +

+
+ setSearchQuery(e.target.value)} + className="w-full bg-white/10 border border-white/30 rounded-lg py-3 px-4 text-white font-display text-[15px] outline-none placeholder:text-white/40 focus:border-white/60 transition-colors" + /> + +
+
+ )} + + {/* Categories panel */} + {activePanel === "categories" && ( +
+

+ Categories +

+
+ {categoryLinks.map(link => ( + setActivePanel(null)} + className={`block rounded-lg py-4 px-4 text-center font-display text-[13px] tracking-[0.5px] uppercase no-underline transition-colors ${ + isActive(link.href) + ? "bg-white/20 text-white font-extrabold" + : "bg-white/8 text-white/80 font-semibold hover:bg-white/15" + }`} + > + {link.label} + + ))} +
+
+ )} + + {/* More panel */} + {activePanel === "more" && ( +
+

+ Menu +

+ {[ + { label: "About Us", href: "/about" }, + { label: "All Posts", href: "/posts" }, + { label: "Tags", href: "/tags" }, + ].map(link => ( + setActivePanel(null)} + className="flex items-center gap-3 py-3.5 px-2 border-b border-white/10 font-display text-[15px] text-white font-semibold no-underline hover:bg-white/5 transition-colors" + > + {link.label} + + ))} +
+

+ Categories +

+ {categoryLinks.map(link => ( + setActivePanel(null)} + className={`flex items-center gap-3 py-3 px-2 border-b border-white/10 font-display text-[14px] no-underline transition-colors ${ + isActive(link.href) + ? "text-white font-extrabold bg-white/8" + : "text-white/80 font-medium hover:bg-white/5" + }`} + > + {link.label} + + ))} +
+
+ )} +
)} + + {/* ── Mobile Bottom Bar ── */} + {compact && ( + + )} ); } diff --git a/src/components/HeaderCard.astro b/src/components/HeaderCard.astro index 0e5ae19..988e7f8 100644 --- a/src/components/HeaderCard.astro +++ b/src/components/HeaderCard.astro @@ -18,15 +18,11 @@ const image = getFirstImage(post.body); > { image ? ( -
- {post.data.title} +
+ {post.data.title}
) : ( -
+
) }
{ image ? ( -
- {post.data.title} +
+ {post.data.title}
) : ( -
+
) }
{ image ? ( -
- {post.data.title} +
+ {post.data.title}
) : ( -
+
) }
diff --git a/src/pages/index.astro b/src/pages/index.astro index 4524745..06accb9 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -57,7 +57,7 @@ const getPostsByCategory = (cat: string) => { { heroPost && (
{heroImage && ( <> @@ -80,7 +80,7 @@ const getPostsByCategory = (cat: string) => { > {heroPost.data.title} -
+
diff --git a/src/styles/base.css b/src/styles/base.css index 82ded24..2a00f85 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -38,6 +38,11 @@ @apply flex min-h-[100svh] flex-col bg-skin-fill font-mono text-skin-base selection:bg-skin-accent selection:bg-opacity-70 selection:text-skin-inverted; } + @media (max-width: 1023px) { + body { + padding-bottom: 64px; + } + } section { @apply mx-auto max-w-3xl px-4; } @@ -141,11 +146,32 @@ @apply outline-2 outline-offset-1 outline-skin-fill focus-visible:no-underline focus-visible:outline-dashed; } - /* Custom JourKnows UI utilities */ + + .jk-img-container { + @apply overflow-hidden rounded-lg; + background-color: #e8e8e8; + background-image: url("/assets/logo.svg"); + background-repeat: no-repeat; + background-position: center; + background-size: 48px 48px; + } + + .jk-img-container img { + @apply w-full h-full object-cover; + transition: transform 0.3s ease; + } + .group:hover .jk-img-container img { + transform: scale(1.05); + } + @keyframes jkFadeUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } } + @keyframes slideInRight { + from { transform: translateX(100%); } + to { transform: translateX(0); } + } .jk-page { animation: jkFadeUp 0.28s ease both; } diff --git a/src/utils/useBreakpoint.ts b/src/utils/useBreakpoint.ts index d5b62cc..d85c408 100644 --- a/src/utils/useBreakpoint.ts +++ b/src/utils/useBreakpoint.ts @@ -1,11 +1,14 @@ import { useState, useEffect } from "react"; export default function useBreakpoint() { - const getW = () => (typeof window !== "undefined" ? window.innerWidth : 1200); - const [w, setW] = useState(getW); + // Always start with 1200 (desktop) for SSR consistency + const [w, setW] = useState(1200); + const [isHydrated, setIsHydrated] = useState(false); useEffect(() => { - if (typeof window === "undefined") return; + setW(window.innerWidth); + setIsHydrated(true); + const h = () => setW(window.innerWidth); window.addEventListener("resize", h); return () => window.removeEventListener("resize", h); @@ -15,6 +18,7 @@ export default function useBreakpoint() { isMobile: w < 640, isTablet: w >= 640 && w < 1024, isDesktop: w >= 1024, + isHydrated, w, }; }