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: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint": "eslint src",
"lint:fix": "eslint src --fix && pnpm format",
"lint:strict": "eslint --max-warnings=0 src",
"typecheck": "tsc --noEmit",
Expand Down
15 changes: 12 additions & 3 deletions src/__tests__/components/layout/Header.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,18 @@ describe('Header Component', () => {
expect(articlesLink).toHaveAttribute('href', '/articles');
});

it('should render Exhibition link', () => {
render(<Header />);
const exhibitionLink = screen.getByRole('link', { name: /exhibition/i });
expect(exhibitionLink).toBeInTheDocument();
expect(exhibitionLink).toHaveAttribute('href', '/exhibition');
});

it('should have all navigation links', () => {
render(<Header />);
const navLinks = screen.getAllByRole('link');
// Should have at least 4 links (logo + 3 nav items)
expect(navLinks.length).toBeGreaterThanOrEqual(4);
// Should have at least 5 links (logo + 4 nav items)
expect(navLinks.length).toBeGreaterThanOrEqual(5);
});
});

Expand Down Expand Up @@ -111,9 +118,11 @@ describe('Header Component', () => {
const aboutLink = screen.getByRole('link', { name: /about/i });
const teamLink = screen.getByRole('link', { name: /team/i });
const articlesLink = screen.getByRole('link', { name: /articles/i });
const exhibitionLink = screen.getByRole('link', { name: /exhibition/i });
expect(aboutLink).toBeInTheDocument();
expect(teamLink).toBeInTheDocument();
expect(articlesLink).toBeInTheDocument();
expect(exhibitionLink).toBeInTheDocument();
});
});

Expand All @@ -127,7 +136,7 @@ describe('Header Component', () => {
it('should render list items for each nav link', () => {
render(<Header />);
const listItems = screen.getAllByRole('listitem');
expect(listItems.length).toBe(3); // About, Team, Articles
expect(listItems.length).toBe(4); // About, Team, Articles, Exhibition
});

it('should use NavLink components', () => {
Expand Down
82 changes: 80 additions & 2 deletions src/components/layout/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
'use client';

import { Menu, X } from 'lucide-react';
import Link from 'next/link';
import * as React from 'react';

import { Logo } from '@/components/ui/icons/Logo';
import NavLink from '@/components/ui/NavLink';
Expand All @@ -8,6 +12,7 @@ const navLinks = [
{ href: '/about', text: 'About' },
{ href: '/team', text: 'Team' },
{ href: '/articles', text: 'Articles' },
{ href: '/exhibition', text: 'Exhibition' },
];

function Navigation() {
Expand All @@ -25,20 +30,93 @@ function Navigation() {
}

export default function Header() {
const [mobileNavOpen, setMobileNavOpen] = React.useState(false);
const mobileNavId = React.useId();
const mobileNavRef = React.useRef<HTMLDivElement | null>(null);

React.useEffect(() => {
if (!mobileNavOpen) return;

function onKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape') setMobileNavOpen(false);
}

function onPointerDown(e: MouseEvent | TouchEvent) {
const target = e.target;
if (!(target instanceof Node)) return;
if (mobileNavRef.current?.contains(target)) return;
setMobileNavOpen(false);
}

document.addEventListener('keydown', onKeyDown);
document.addEventListener('mousedown', onPointerDown);
document.addEventListener('touchstart', onPointerDown, { passive: true });

return () => {
document.removeEventListener('keydown', onKeyDown);
document.removeEventListener('mousedown', onPointerDown);
document.removeEventListener('touchstart', onPointerDown);
};
}, [mobileNavOpen]);

return (
<header className='pointer-events-none fixed z-30 flex w-full justify-center'>
<div className='pointer-events-auto relative mt-8 flex items-center justify-between gap-3 overflow-hidden rounded-full border-[1px] border-solid border-black bg-white/85 px-2 py-2 pl-4 shadow-sm backdrop-blur-md dark:border-white/20 dark:bg-neutral-950/85 sm:gap-32'>
<div className='pointer-events-auto relative mt-8 flex items-center justify-between gap-3 rounded-full border-[1px] border-solid border-black bg-white/85 px-2 py-2 pl-4 shadow-sm backdrop-blur-md dark:border-white/20 dark:bg-neutral-950/85 sm:gap-32'>
<Link
href='/'
aria-label='Go to homepage'
className='text-black ease-in-out hover:text-primary dark:text-white dark:hover:text-primary'
>
<Logo className='w-24 sm:w-48' variant='textOnly' />
</Link>

<div className='flex items-center gap-1'>
<Navigation />
<div className='hidden sm:block'>
<Navigation />
</div>

<button
type='button'
className='inline-flex h-8 w-8 items-center justify-center rounded-2xl px-2 py-1 text-sm transition-colors hover:bg-neutral-200/75 hover:text-black focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-bg dark:hover:bg-neutral-700/75 dark:hover:text-white sm:hidden'
aria-label={mobileNavOpen ? 'Close menu' : 'Open menu'}
aria-expanded={mobileNavOpen}
aria-controls={mobileNavId}
onClick={() => setMobileNavOpen((v) => !v)}
>
{mobileNavOpen ? (
<X className='h-4 w-4' aria-hidden='true' />
) : (
<Menu className='h-4 w-4' aria-hidden='true' />
)}
</button>

<ThemeToggle />
</div>

<div
id={mobileNavId}
ref={mobileNavRef}
className='absolute left-0 top-full mt-2 w-full px-2 sm:hidden'
hidden={!mobileNavOpen}
>
<nav
aria-label='Mobile navigation'
className='rounded-3xl border border-black bg-white/95 p-2 shadow-sm backdrop-blur-md dark:border-white/20 dark:bg-neutral-950/95'
>
<ul className='flex flex-col gap-1'>
{navLinks.map((link) => (
<li key={link.href}>
<NavLink
href={link.href}
onClick={() => setMobileNavOpen(false)}
>
{link.text}
</NavLink>
</li>
))}
</ul>
</nav>
</div>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>
</header>
);
Expand Down
3 changes: 3 additions & 0 deletions src/components/ui/NavLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@ import clsxm from '@/lib/clsxm';
const NavLink = ({
href,
children,
onClick,
}: {
href: string;
children: React.ReactNode;
onClick?: React.MouseEventHandler<HTMLAnchorElement>;
}) => {
const pathname = usePathname();
const isActive = href === '/' ? pathname === '/' : pathname.startsWith(href);

return (
<Link
href={href}
onClick={onClick}
className={clsxm(
'rounded-2xl px-2 py-1 text-sm hover:bg-neutral-200/75 hover:text-black focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-bg dark:hover:bg-neutral-700/75 dark:hover:text-white sm:text-lg',
isActive && 'bg-black text-white dark:bg-white dark:text-black',
Expand Down
32 changes: 32 additions & 0 deletions vercel.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"rewrites": [
{
"source": "/api/events",
"destination": "https://sentiment-exhibition.vercel.app/api/events/"
},
{
"source": "/api/events/",
"destination": "https://sentiment-exhibition.vercel.app/api/events/"
},
{
"source": "/api/events/:path*",
"destination": "https://sentiment-exhibition.vercel.app/api/events/:path*"
},
{
"source": "/public/:path*",
"destination": "https://sentiment-exhibition.vercel.app/public/:path*"
},
{
"source": "/exhibition",
"destination": "https://sentiment-exhibition.vercel.app/"
},
{
"source": "/exhibition/",
"destination": "https://sentiment-exhibition.vercel.app/"
},
{
"source": "/exhibition/:path*",
"destination": "https://sentiment-exhibition.vercel.app/:path*"
}
]
Comment on lines +2 to +31
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the exhibition app serves a Next.js HTML shell referencing /_next/*
curl -s -L https://sentiment-exhibition.vercel.app/ | grep -oE '"/_next/[^"]+"|href="/[^"]+"|src="/[^"]+"' | sort -u | head -40

Repository: dmnktoe/sentiment

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Get response headers and first part of HTML to identify the app type
curl -s -I https://sentiment-exhibition.vercel.app/ | head -20
echo "---"
curl -s https://sentiment-exhibition.vercel.app/ | head -100

Repository: dmnktoe/sentiment

Length of output: 5142


🏁 Script executed:

#!/bin/bash
# Check if the domain is accessible and what it serves
curl -s -w "\nHTTP Status: %{http_code}\n" https://sentiment-exhibition.vercel.app/ | head -150

Repository: dmnktoe/sentiment

Length of output: 6606


Relative asset paths in the exhibition app will 404 under the current rewrites.

The exhibition app at sentiment-exhibition.vercel.app is a static/traditional website (not Next.js) that references assets via relative paths: public/css/main.css, public/img/logo.svg, public/img/stage-video.mp4, etc.

When served via /exhibition, the browser URL remains /exhibition, so relative asset paths resolve to /exhibition/public/css/main.css, /exhibition/public/img/logo.svg, etc. These do not match the /public/:path* rewrite (which only catches /public/* at the root), causing assets to 404 or serve from the wrong origin.

Either:

  • Reconfigure the exhibition app to use absolute paths (/public/...) instead of relative paths, then ensure the /public/:path* rewrite is in place (it already is), or
  • Restructure the rewrite rules to handle /exhibition/:path*sentiment-exhibition.vercel.app/:path* to preserve all sub-paths including /exhibition/public/*.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vercel.json` around lines 2 - 31, The rewrites currently cause relative asset
requests like /exhibition/public/... to miss the /public/:path* rule; either
update the exhibition app to use absolute asset URLs (/public/...) and keep the
existing /public/:path* rewrite, or add a specific rewrite mapping
"/exhibition/public/:path*" ->
"https://sentiment-exhibition.vercel.app/public/:path*" (or otherwise ensure
"/exhibition/:path*" correctly strips the /exhibition prefix) so assets
requested under /exhibition/public/* are forwarded to the exhibition origin.

}