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 */}
+
+

+

+
+ {/* Mobile: slim loading header while JS initializes */}
+
+

+
+ >
+ );
+ }
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 && (
-
-
-
- )}
- {/* 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
+
+
+
+ )}
+
+ {/* Categories panel */}
+ {activePanel === "categories" && (
+
+ )}
+
+ {/* More panel */}
+ {activePanel === "more" && (
+
+ )}
+
)}
+
+ {/* ── 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 ? (
-
-

+
+
) : (
-
+
)
}
{
image ? (
-
-

+
+
) : (
-
+
)
}
{
image ? (
-
-

+
+
) : (
-
+
)
}
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,
};
}