Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ npx shadcn@latest view button card # Preview code before installing
npx shadcn@latest add <component> # Add component to project
npx shadcn@latest add button --overwrite # Overwrite existing component
npx shadcn@latest add @v0/<block> # Add from v0.dev registry
npx shadcn@latest diff # Check for upstream registry updates
npx shadcn@latest --help # CLI help
```

## Coding Practices
Expand Down
29 changes: 15 additions & 14 deletions app/(root)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,36 @@
import { cookies } from "next/headers";
import { AppSidebar } from "@/components/app-sidebar";
import { ContentTopBar } from "@/components/navigation/content-top-bar";
import { MobileHeader } from "@/components/navigation/mobile-header";
import { DesktopTopBar } from "@/components/navigation/desktop-top-bar";
import { LeftSidebar } from "@/components/navigation/left-sidebar";
import { MobileTopBar } from "@/components/navigation/mobile-top-bar";
import { SidebarProvider } from "@/components/ui/sidebar";
import {
CONTENT_HORIZONTAL_PADDING,
cn,
MOBILE_HEADER_TOP_OFFSET,
LAYOUT_HORIZONTAL_PADDING,
MOBILE_TOP_BAR_OFFSET,
} from "@/lib/utils";

const RootLayout = async ({ children }: { children: React.ReactNode }) => {
const cookieStore = await cookies();
const defaultOpen = cookieStore.get("sidebar_state")?.value !== "false";
const sidebarOpenFromCookie =
cookieStore.get("sidebar_state")?.value !== "false";

return (
<SidebarProvider defaultOpen={defaultOpen}>
{/* Mobile-only fixed header */}
<MobileHeader />
<SidebarProvider defaultOpen={sidebarOpenFromCookie}>
{/* Mobile-only top bar */}
<MobileTopBar />

{/* Full-height sidebar */}
<AppSidebar />
<LeftSidebar />

{/* Content area */}
{/* Main content area */}
<div className="flex min-h-screen flex-1 flex-col">
{/* Desktop-only top bar */}
<ContentTopBar />
<DesktopTopBar />
<main
className={cn(
"flex-1 pb-10 sm:pt-10",
MOBILE_HEADER_TOP_OFFSET,
CONTENT_HORIZONTAL_PADDING,
MOBILE_TOP_BAR_OFFSET,
LAYOUT_HORIZONTAL_PADDING,
)}
>
{children}
Expand Down
28 changes: 17 additions & 11 deletions app/(root)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
const Home = () => (
const HomePage = () => (
<>
<h1>Hello Root page with heading H1</h1>

{/* Header boundary marker */}
{/* Demo: top overflow test */}
<div className="mt-8 bg-primary/30 p-4">{"words ".repeat(50)}</div>

{/* Flexbox demo: grow vs flex-none */}
Expand All @@ -23,22 +23,22 @@ const Home = () => (
</div>
</div>

{/* Footer boundary marker */}
{/* Demo: bottom overflow test */}
<div className="bg-primary/30 p-4">{"words ".repeat(50)}</div>

{/* Question boxes for testing sticky sidebar scroll */}
<div className="mt-8 space-y-6">
{[1, 2, 3, 4].map((num) => (
{[1, 2, 3, 4].map((questionIndex) => (
<article
key={`question-${num}`}
key={`question-${questionIndex}`}
className="rounded-lg border border-border bg-card p-6 shadow-sm"
>
<div className="mb-3 flex items-center gap-2">
<span className="rounded-full bg-primary/10 px-3 py-1 text-xs font-medium text-primary">
Question #{num}
Question #{questionIndex}
</span>
<span className="text-xs text-muted-foreground">
Asked {num} hours ago
Asked {questionIndex} hours ago
</span>
</div>

Expand Down Expand Up @@ -69,15 +69,21 @@ const Home = () => (
{/* Stats */}
<div className="flex items-center gap-6 text-sm text-muted-foreground">
<span className="flex items-center gap-1">
<span className="font-semibold text-foreground">{num * 7}</span>{" "}
<span className="font-semibold text-foreground">
{questionIndex * 7}
</span>{" "}
votes
</span>
<span className="flex items-center gap-1">
<span className="font-semibold text-foreground">{num * 3}</span>{" "}
<span className="font-semibold text-foreground">
{questionIndex * 3}
</span>{" "}
answers
</span>
<span className="flex items-center gap-1">
<span className="font-semibold text-foreground">{num * 47}</span>{" "}
<span className="font-semibold text-foreground">
{questionIndex * 47}
</span>{" "}
views
</span>
</div>
Expand All @@ -92,4 +98,4 @@ const Home = () => (
</>
);

export default Home;
export default HomePage;
4 changes: 2 additions & 2 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Metadata } from "next";
import { Suspense } from "react";
import { inter, jetbrainsMono, spaceGrotesk } from "@/app/fonts";
import { ClerkProvider } from "@/components/clerk-provider";
import { ThemeProvider } from "@/components/theme-provider";
import { ClerkProvider } from "@/components/providers/clerk-provider";
import { ThemeProvider } from "@/components/providers/theme-provider";
import "@/app/globals.css";

export const metadata: Metadata = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
import { SignedOut, SignInButton, SignUpButton } from "@clerk/nextjs";
import { ThemeToggle } from "@/components/navigation/theme-toggle";
import { Button } from "@/components/ui/button";
import { CONTENT_HORIZONTAL_PADDING, cn, HEADER_HEIGHT } from "@/lib/utils";
import { cn, LAYOUT_HORIZONTAL_PADDING, TOP_BAR_HEIGHT } from "@/lib/utils";

export function ContentTopBar() {
export function DesktopTopBar() {
return (
<header
className={cn(
"sticky top-0 z-40 hidden items-center gap-4 border-b bg-background sm:flex",
HEADER_HEIGHT,
CONTENT_HORIZONTAL_PADDING,
TOP_BAR_HEIGHT,
LAYOUT_HORIZONTAL_PADDING,
)}
>
{/* Left: Search - grows and centres */}
<div className="flex flex-1 justify-center">
<p className="text-muted-foreground">Global Search</p>
</div>

{/* Right: Theme + Auth - fixed width */}
<div className="flex flex-none items-center gap-2">
<ThemeToggle />
<SignedOut>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button";
import { useSidebar } from "@/components/ui/sidebar";
import { cn } from "@/lib/utils";

export function SidebarToggleButton() {
export function LeftSidebarToggle() {
const { state, toggleSidebar } = useSidebar();
const isCollapsed = state === "collapsed";

Expand All @@ -18,9 +18,9 @@ export function SidebarToggleButton() {
"shrink-0 rounded-full text-sidebar-foreground",
!isCollapsed && "bg-muted",
)}
aria-label={isCollapsed ? "Expand sidebar" : "Collapse sidebar"}
aria-label={isCollapsed ? "Expand left sidebar" : "Collapse left sidebar"}
aria-expanded={!isCollapsed}
aria-controls="app-sidebar"
aria-controls="left-sidebar"
>
{isCollapsed ? (
<ChevronsRight className="size-5" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { SignedIn, UserButton } from "@clerk/nextjs";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { ThemeLogo } from "@/components/navigation/full-logo";
import { LeftSidebarToggle } from "@/components/navigation/left-sidebar-toggle";
import { NAV_LINKS } from "@/components/navigation/nav-links.constants";
import { SidebarToggleButton } from "@/components/navigation/sidebar-toggle-button";
import { ThemedFullLogo } from "@/components/navigation/themed-full-logo";
import {
Sidebar,
SidebarContent,
Expand All @@ -21,18 +21,18 @@ import {
} from "@/components/ui/sidebar";
import {
cn,
getNavIconClasses,
getNavIconInvertClasses,
isRouteActive,
NAV_LINK_ACTIVE_CLASSES,
NAV_LINK_INACTIVE_CLASSES,
} from "@/lib/utils";

export function AppSidebar() {
export function LeftSidebar() {
const pathname = usePathname();

return (
<Sidebar id="app-sidebar" collapsible="icon">
{/* Header: Logo - h-14 matches ContentTopBar height */}
<Sidebar id="left-sidebar" collapsible="icon">
{/* Logo section - h-14 matches top bar height */}
<SidebarHeader className="h-14 flex-row items-center px-3">
<Link
href="/"
Expand All @@ -47,7 +47,7 @@ export function AppSidebar() {
className="size-6 group-data-[collapsible=icon]:block hidden"
/>
{/* Full logo when expanded */}
<ThemeLogo className="group-data-[collapsible=icon]:hidden" />
<ThemedFullLogo className="group-data-[collapsible=icon]:hidden" />
</Link>
</SidebarHeader>

Expand All @@ -74,11 +74,11 @@ export function AppSidebar() {
>
<Link href={link.route}>
<Image
src={link.imgURL}
src={link.iconUrl}
alt=""
width={20}
height={20}
className={getNavIconClasses(isActive)}
className={getNavIconInvertClasses(isActive)}
/>
<span>{link.label}</span>
</Link>
Expand Down Expand Up @@ -106,7 +106,7 @@ export function AppSidebar() {
<SignedIn>
<UserButton />
</SignedIn>
<SidebarToggleButton />
<LeftSidebarToggle />
</div>
</SidebarFooter>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,24 @@ import { usePathname } from "next/navigation";
import type { NavLink as NavLinkType } from "@/components/navigation/nav-links.constants";
import {
cn,
getNavIconClasses,
getNavIconInvertClasses,
isRouteActive,
NAV_LINK_ACTIVE_CLASSES,
NAV_LINK_INACTIVE_CLASSES,
} from "@/lib/utils";

type NavLinkProps = NavLinkType & {
type MobileNavLinkProps = NavLinkType & {
/** Passed by SheetClose asChild to close the sheet on click */
onClick?: () => void;
};

/** Navigation link for mobile Sheet menu. */
export function NavLink({ imgURL, route, label, onClick }: NavLinkProps) {
export function MobileNavLink({
iconUrl,
route,
label,
onClick,
}: MobileNavLinkProps) {
const pathname = usePathname();
const isActive = isRouteActive(pathname, route);

Expand All @@ -34,11 +39,11 @@ export function NavLink({ imgURL, route, label, onClick }: NavLinkProps) {
)}
>
<Image
src={imgURL}
src={iconUrl}
alt=""
width={20}
height={20}
className={getNavIconClasses(isActive)}
className={getNavIconInvertClasses(isActive)}
/>
<span>{label}</span>
</Link>
Expand Down
10 changes: 5 additions & 5 deletions components/navigation/mobile-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import {
import { Menu, X } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { ThemeLogo } from "@/components/navigation/full-logo";
import { NavLink } from "@/components/navigation/nav-link";
import { MobileNavLink } from "@/components/navigation/mobile-nav-link";
import { NAV_LINKS } from "@/components/navigation/nav-links.constants";
import { ThemedFullLogo } from "@/components/navigation/themed-full-logo";
import { Button } from "@/components/ui/button";
import {
Sheet,
Expand Down Expand Up @@ -66,16 +66,16 @@ export function MobileNav() {
<SheetTitle className="sr-only">Mobile navigation menu</SheetTitle>
<SheetClose asChild>
<Link href="/" className="flex items-center">
<ThemeLogo />
<ThemedFullLogo />
</Link>
</SheetClose>
</SheetHeader>

<nav className="flex flex-1 flex-col gap-3 pt-5">
{NAV_LINKS.map((link) => (
<SheetClose key={link.route} asChild>
<NavLink
imgURL={link.imgURL}
<MobileNavLink
iconUrl={link.iconUrl}
route={link.route}
label={link.label}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
import Link from "next/link";
import { MobileNav } from "@/components/navigation/mobile-nav";
import { ThemeToggle } from "@/components/navigation/theme-toggle";
import { cn, HEADER_HEIGHT } from "@/lib/utils";
import { cn, TOP_BAR_HEIGHT } from "@/lib/utils";

export function MobileHeader() {
export function MobileTopBar() {
return (
<header
className={cn(
"fixed top-0 z-50 flex w-full items-center justify-between bg-sidebar px-4 shadow-sm sm:hidden",
HEADER_HEIGHT,
TOP_BAR_HEIGHT,
)}
>
{/* Left: Logo icon */}
<Link href="/" aria-label="DevFlow mobile logo">
{/* biome-ignore lint/a11y/useAltText: Decorative logo, aria-label on parent link */}
{/* biome-ignore lint/performance/noImgElement: SVG logo doesn't benefit from next/image optimisation */}
<img src="/images/site-logo.svg" className="size-7" />
</Link>

{/* Right: Theme toggle + hamburger */}
<div className="flex items-center gap-2">
<ThemeToggle />
<MobileNav />
Expand Down
14 changes: 7 additions & 7 deletions components/navigation/nav-links.constants.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import type { Route } from "next";

export type NavLink = {
imgURL: string;
iconUrl: string;
route: Route;
label: string;
};

export const NAV_LINKS = [
{ imgURL: "/icons/home.svg", route: "/", label: "Home" },
{ imgURL: "/icons/users.svg", route: "/community", label: "Community" },
{ imgURL: "/icons/star.svg", route: "/collections", label: "Collections" },
{ imgURL: "/icons/suitcase.svg", route: "/jobs", label: "Find Jobs" },
{ imgURL: "/icons/tag.svg", route: "/tags", label: "Tags" },
{ iconUrl: "/icons/home.svg", route: "/", label: "Home" },
{ iconUrl: "/icons/users.svg", route: "/community", label: "Community" },
{ iconUrl: "/icons/star.svg", route: "/collections", label: "Collections" },
{ iconUrl: "/icons/suitcase.svg", route: "/jobs", label: "Find Jobs" },
{ iconUrl: "/icons/tag.svg", route: "/tags", label: "Tags" },
{
imgURL: "/icons/question.svg",
iconUrl: "/icons/question.svg",
route: "/ask-question",
label: "Ask a question",
},
Expand Down
Loading
Loading