diff --git a/resources/js/components/partials/header.test.tsx b/resources/js/components/partials/header.test.tsx
index 8a10e456..99029d62 100644
--- a/resources/js/components/partials/header.test.tsx
+++ b/resources/js/components/partials/header.test.tsx
@@ -1,10 +1,11 @@
-import { dashboard } from '@/routes';
+import { dashboard, home } from '@/routes';
import { render, screen } from '@testing-library/react';
import type React from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import Header from './header';
const mocks = vi.hoisted(() => ({
+ url: '/roadmap',
pageProps: {
auth: {
user: {
@@ -24,11 +25,12 @@ vi.mock('@inertiajs/react', () => ({
children: React.ReactNode;
href: string | { url: string };
}) => {children},
- usePage: () => ({ props: mocks.pageProps }),
+ usePage: () => ({ url: mocks.url, props: mocks.pageProps }),
}));
describe('Header', () => {
beforeEach(() => {
+ mocks.url = '/roadmap';
vi.stubGlobal(
'fetch',
vi.fn(() => new Promise(() => {})),
@@ -45,4 +47,26 @@ describe('Header', () => {
expect(dashboardLinks).toHaveLength(2);
expect(dashboardLinks[0]?.getAttribute('href')).toBe(dashboard().url);
});
+
+ it('links the logo home on every page but the landing one', () => {
+ render();
+
+ const logoLinks = screen.getAllByRole('link', {
+ name: 'Whisper Money',
+ });
+
+ expect(logoLinks).toHaveLength(2);
+ expect(logoLinks[0]?.getAttribute('href')).toBe(home().url);
+ });
+
+ it('does not link the logo on the landing page', () => {
+ mocks.url = '/?ref=newsletter';
+
+ render();
+
+ expect(screen.getAllByText('Whisper Money')).toHaveLength(2);
+ expect(
+ screen.queryByRole('link', { name: 'Whisper Money' }),
+ ).toBeNull();
+ });
});
diff --git a/resources/js/components/partials/header.tsx b/resources/js/components/partials/header.tsx
index 3357229a..9a5f94fd 100644
--- a/resources/js/components/partials/header.tsx
+++ b/resources/js/components/partials/header.tsx
@@ -1,9 +1,10 @@
-import { dashboard, login, roadmap } from '@/routes';
+import { cn, resolveUrl } from '@/lib/utils';
+import { dashboard, home, login, roadmap } from '@/routes';
import { type SharedData } from '@/types';
import { __ } from '@/utils/i18n';
import { Link, usePage } from '@inertiajs/react';
import { BirdIcon, Github, LogIn, MapIcon, StarIcon } from 'lucide-react';
-import { useEffect, useState } from 'react';
+import { useEffect, useState, type ReactNode } from 'react';
import DiscordIcon from '../icons/DiscordIcon';
import { Button } from '../ui/button';
import { Separator } from '../ui/separator';
@@ -27,6 +28,56 @@ function useGitHubStars(): number | null {
return stars;
}
+/**
+ * Below `lg` the nav only has room for the icons, so the label stays for
+ * screen readers instead of disappearing with the text.
+ */
+function NavButton({
+ label,
+ badge,
+ children,
+}: {
+ label: string;
+ badge?: ReactNode;
+ children: ReactNode;
+}) {
+ return (
+
+ );
+}
+
+/**
+ * The logo links back to the landing page, except on the landing page itself.
+ */
+function Logo({ className }: { className?: string }) {
+ const { url } = usePage();
+ const isLandingPage = url.split('?')[0] === resolveUrl(home());
+ const classes = cn('flex shrink-0 items-center font-mono', className);
+ const content = (
+ <>
+
+ Whisper Money
+ >
+ );
+
+ if (isLandingPage) {
+ return
{content}
;
+ }
+
+ return (
+
+ {content}
+
+ );
+}
+
type Props = {
canRegister?: boolean;
hideExternalButtons?: boolean;
@@ -43,59 +94,8 @@ export default function Header({
<>
{/* Mobile pill header */}