chore: remove outdated code
This commit is contained in:
@@ -1,4 +0,0 @@
|
|||||||
export type { NavItem } from './model/types';
|
|
||||||
export { MobileNav } from './ui/MobileNav';
|
|
||||||
export { SidebarNav } from './ui/SidebarNav';
|
|
||||||
export { UtilityBar } from './ui/UtilityBar';
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
export type NavItem = {
|
|
||||||
/**
|
|
||||||
* Section HTML id for anchor scrolling
|
|
||||||
*/
|
|
||||||
id: string;
|
|
||||||
/**
|
|
||||||
* Display label
|
|
||||||
*/
|
|
||||||
label: string;
|
|
||||||
/**
|
|
||||||
* Display number prefix (e.g. "01")
|
|
||||||
*/
|
|
||||||
number: string;
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
|
||||||
import { MobileNav } from './MobileNav';
|
|
||||||
|
|
||||||
// MobileNav is lg:hidden — it renders only on mobile viewports.
|
|
||||||
// Use the viewport toolbar in Storybook to switch to a mobile size to see it.
|
|
||||||
const meta: Meta<typeof MobileNav> = {
|
|
||||||
title: 'Widgets/MobileNav',
|
|
||||||
component: MobileNav,
|
|
||||||
parameters: {
|
|
||||||
viewport: {
|
|
||||||
defaultViewport: 'mobile1',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
|
|
||||||
type Story = StoryObj<typeof MobileNav>;
|
|
||||||
|
|
||||||
export const Default: Story = {
|
|
||||||
args: {
|
|
||||||
items: [
|
|
||||||
{ id: 'bio', label: 'Bio', number: '01' },
|
|
||||||
{ id: 'work', label: 'Work', number: '02' },
|
|
||||||
{ id: 'contact', label: 'Contact', number: '03' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
|
||||||
import userEvent from '@testing-library/user-event';
|
|
||||||
import type { NavItem } from '../model/types';
|
|
||||||
import { MobileNav } from './MobileNav';
|
|
||||||
|
|
||||||
vi.mock('next/navigation', () => ({ usePathname: vi.fn(() => '/') }));
|
|
||||||
|
|
||||||
const ITEMS: NavItem[] = [
|
|
||||||
{ id: 'intro', label: 'Intro', number: '01' },
|
|
||||||
{ id: 'bio', label: 'Bio', number: '02' },
|
|
||||||
];
|
|
||||||
|
|
||||||
describe('MobileNav', () => {
|
|
||||||
describe('rendering', () => {
|
|
||||||
it('renders title "allmy.work"', () => {
|
|
||||||
render(<MobileNav items={ITEMS} />);
|
|
||||||
expect(screen.getByText('allmy.work')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders toggle button with text "Menu" initially', () => {
|
|
||||||
render(<MobileNav items={ITEMS} />);
|
|
||||||
expect(screen.getByRole('button', { name: 'Menu' })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('menu items are hidden initially', () => {
|
|
||||||
render(<MobileNav items={ITEMS} />);
|
|
||||||
expect(screen.queryByRole('link', { name: /intro/i })).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('navigation items', () => {
|
|
||||||
it('shows items as links with correct hrefs when open', async () => {
|
|
||||||
render(<MobileNav items={ITEMS} />);
|
|
||||||
await userEvent.click(screen.getByRole('button', { name: 'Menu' }));
|
|
||||||
expect(screen.getByRole('link', { name: /01.*Intro/i })).toHaveAttribute('href', '/intro');
|
|
||||||
expect(screen.getByRole('link', { name: /02.*Bio/i })).toHaveAttribute('href', '/bio');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('interactions', () => {
|
|
||||||
it('click toggle shows links and changes label to "Close"', async () => {
|
|
||||||
render(<MobileNav items={ITEMS} />);
|
|
||||||
await userEvent.click(screen.getByRole('button', { name: 'Menu' }));
|
|
||||||
expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Intro')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('closes menu when pathname changes', async () => {
|
|
||||||
const { usePathname } = await import('next/navigation');
|
|
||||||
vi.mocked(usePathname).mockReturnValue('/');
|
|
||||||
const { rerender } = render(<MobileNav items={ITEMS} />);
|
|
||||||
await userEvent.click(screen.getByRole('button', { name: 'Menu' }));
|
|
||||||
expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument();
|
|
||||||
|
|
||||||
vi.mocked(usePathname).mockReturnValue('/bio');
|
|
||||||
rerender(<MobileNav items={ITEMS} />);
|
|
||||||
|
|
||||||
expect(screen.getByRole('button', { name: 'Menu' })).toBeInTheDocument();
|
|
||||||
expect(screen.queryByRole('button', { name: 'Close' })).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { cn } from '$shared/lib';
|
|
||||||
import type { NavItem } from '../model/types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Props for MobileNav.
|
|
||||||
*/
|
|
||||||
interface Props {
|
|
||||||
/**
|
|
||||||
* Navigation items to render
|
|
||||||
*/
|
|
||||||
items: NavItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mobile navigation overlay, hidden on lg+ screens.
|
|
||||||
* Closes automatically when the URL pathname changes after navigation.
|
|
||||||
*/
|
|
||||||
export function MobileNav({ items }: Props) {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: pathname is the trigger, not a value used inside the callback
|
|
||||||
useEffect(() => {
|
|
||||||
setIsOpen(false);
|
|
||||||
}, [pathname]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="lg:hidden fixed top-0 left-0 right-0 bg-cream brutal-border-bottom z-50">
|
|
||||||
<div className="px-6 py-4 flex items-center justify-between">
|
|
||||||
<h4>allmy.work</h4>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsOpen((prev) => !prev)}
|
|
||||||
className="brutal-border px-4 py-2 bg-blue text-cream"
|
|
||||||
>
|
|
||||||
{isOpen ? 'Close' : 'Menu'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{isOpen && (
|
|
||||||
<div className="px-6 py-6 brutal-border-top space-y-2 max-h-[80vh] overflow-y-auto">
|
|
||||||
{items.map((item) => (
|
|
||||||
<Link key={item.id} href={`/${item.id}`} className="block w-full brutal-border bg-cream px-4 py-3">
|
|
||||||
<div className={cn('flex items-baseline gap-3')}>
|
|
||||||
<span className="text-sm opacity-60 font-body">{item.number}</span>
|
|
||||||
<span
|
|
||||||
className="font-heading text-lg font-black"
|
|
||||||
style={{ fontVariationSettings: '"WONK" 1, "SOFT" 0' }}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
|
||||||
import { SidebarNav } from './SidebarNav';
|
|
||||||
|
|
||||||
// SidebarNav is hidden lg:block — it renders only on desktop viewports.
|
|
||||||
// Use the viewport toolbar in Storybook to switch to a desktop size to see it.
|
|
||||||
const meta: Meta<typeof SidebarNav> = {
|
|
||||||
title: 'Widgets/SidebarNav',
|
|
||||||
component: SidebarNav,
|
|
||||||
parameters: {
|
|
||||||
layout: 'fullscreen',
|
|
||||||
viewport: {
|
|
||||||
defaultViewport: 'desktop',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
|
|
||||||
type Story = StoryObj<typeof SidebarNav>;
|
|
||||||
|
|
||||||
export const Default: Story = {
|
|
||||||
args: {
|
|
||||||
items: [
|
|
||||||
{ id: 'bio', label: 'Bio', number: '01' },
|
|
||||||
{ id: 'work', label: 'Work', number: '02' },
|
|
||||||
{ id: 'contact', label: 'Contact', number: '03' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
|
||||||
import type { NavItem } from '../model/types';
|
|
||||||
import { SidebarNav } from './SidebarNav';
|
|
||||||
|
|
||||||
vi.mock('next/navigation', () => ({
|
|
||||||
usePathname: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
|
|
||||||
const ITEMS: NavItem[] = [
|
|
||||||
{ id: 'bio', label: 'Bio', number: '01' },
|
|
||||||
{ id: 'work', label: 'Work', number: '02' },
|
|
||||||
];
|
|
||||||
|
|
||||||
describe('SidebarNav', () => {
|
|
||||||
describe('rendering', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.mocked(usePathname).mockReturnValue('/bio');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders a nav element', () => {
|
|
||||||
render(<SidebarNav items={ITEMS} />);
|
|
||||||
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders "Index" heading', () => {
|
|
||||||
render(<SidebarNav items={ITEMS} />);
|
|
||||||
expect(screen.getByText('Index')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders "Digital Monograph" subtitle', () => {
|
|
||||||
render(<SidebarNav items={ITEMS} />);
|
|
||||||
expect(screen.getByText('Digital Monograph')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders each item label and number', () => {
|
|
||||||
render(<SidebarNav items={ITEMS} />);
|
|
||||||
expect(screen.getByText('Bio')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('01')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Work')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('02')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders "Quick Links" section', () => {
|
|
||||||
render(<SidebarNav items={ITEMS} />);
|
|
||||||
expect(screen.getByText('Quick Links')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders Email quick link', () => {
|
|
||||||
render(<SidebarNav items={ITEMS} />);
|
|
||||||
expect(screen.getByRole('link', { name: 'Email' })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders a link for each nav item', () => {
|
|
||||||
render(<SidebarNav items={ITEMS} />);
|
|
||||||
expect(screen.getByRole('link', { name: /Bio/i })).toBeInTheDocument();
|
|
||||||
expect(screen.getByRole('link', { name: /Work/i })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('active state', () => {
|
|
||||||
it('marks matching pathname item as active (no opacity-40)', () => {
|
|
||||||
vi.mocked(usePathname).mockReturnValue('/bio');
|
|
||||||
render(<SidebarNav items={ITEMS} />);
|
|
||||||
const activeLink = screen.getByRole('link', { name: /Bio/i });
|
|
||||||
expect(activeLink).not.toHaveClass('opacity-40');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('marks non-matching item as inactive (opacity-40)', () => {
|
|
||||||
vi.mocked(usePathname).mockReturnValue('/bio');
|
|
||||||
render(<SidebarNav items={ITEMS} />);
|
|
||||||
const inactiveLink = screen.getByRole('link', { name: /Work/i });
|
|
||||||
expect(inactiveLink).toHaveClass('opacity-40');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('marks first item active at root path', () => {
|
|
||||||
vi.mocked(usePathname).mockReturnValue('/');
|
|
||||||
render(<SidebarNav items={ITEMS} />);
|
|
||||||
const firstLink = screen.getByRole('link', { name: /Bio/i });
|
|
||||||
expect(firstLink).not.toHaveClass('opacity-40');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import { CONTACT_LINKS, cn } from '$shared/lib';
|
|
||||||
import type { NavItem } from '../model/types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Props for SidebarNav.
|
|
||||||
*/
|
|
||||||
interface Props {
|
|
||||||
/**
|
|
||||||
* Navigation items to render
|
|
||||||
*/
|
|
||||||
items: NavItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fixed sidebar navigation, visible on lg+ screens.
|
|
||||||
* Active section determined by current URL pathname.
|
|
||||||
*/
|
|
||||||
export function SidebarNav({ items }: Props) {
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An item is active when its slug matches the current pathname,
|
|
||||||
* or when the pathname is root and it is the first item.
|
|
||||||
*/
|
|
||||||
function isActive(item: NavItem): boolean {
|
|
||||||
if (pathname === `/${item.id}`) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (pathname === '/' && items[0]?.id === item.id) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<nav className="fixed top-0 left-0 h-screen w-full lg:w-1/3 bg-cream brutal-border-right hidden lg:block overflow-y-auto z-50">
|
|
||||||
<div className="px-8 py-12 space-y-2">
|
|
||||||
<div className="mb-12">
|
|
||||||
<h2>Index</h2>
|
|
||||||
<div className="brutal-border-top pt-4">
|
|
||||||
<p className="text-sm opacity-60">Digital Monograph</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{items.map((item) => (
|
|
||||||
<Link
|
|
||||||
key={item.id}
|
|
||||||
href={`/${item.id}`}
|
|
||||||
className={cn(
|
|
||||||
'block w-full text-left brutal-border bg-cream px-6 py-4 transition-all duration-300',
|
|
||||||
isActive(item)
|
|
||||||
? 'shadow-brutal-2xl opacity-100 translate-x-0'
|
|
||||||
: 'opacity-40 shadow-none hover:opacity-60',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-baseline gap-4">
|
|
||||||
<span className="text-sm opacity-60">{item.number}</span>
|
|
||||||
<span className="font-heading text-xl font-black">{item.label}</span>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<div className="mt-12 pt-12 brutal-border-top">
|
|
||||||
<p className="text-sm uppercase tracking-wider mb-4 opacity-60">Quick Links</p>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<a href={`mailto:${CONTACT_LINKS.email}`} className="block">
|
|
||||||
Email
|
|
||||||
</a>
|
|
||||||
<a href={CONTACT_LINKS.linkedin} className="block">
|
|
||||||
LinkedIn
|
|
||||||
</a>
|
|
||||||
<a href={CONTACT_LINKS.instagram} className="block">
|
|
||||||
Instagram
|
|
||||||
</a>
|
|
||||||
<a href={CONTACT_LINKS.arena} className="block">
|
|
||||||
Are.na
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
|
||||||
import { UtilityBar } from './UtilityBar';
|
|
||||||
|
|
||||||
const meta: Meta<typeof UtilityBar> = {
|
|
||||||
title: 'Widgets/UtilityBar',
|
|
||||||
component: UtilityBar,
|
|
||||||
decorators: [
|
|
||||||
(Story) => (
|
|
||||||
<div className="relative h-24 bg-ochre-clay">
|
|
||||||
<Story />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
|
|
||||||
type Story = StoryObj<typeof UtilityBar>;
|
|
||||||
|
|
||||||
export const Default: Story = {};
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
|
||||||
import { UtilityBar } from './UtilityBar';
|
|
||||||
|
|
||||||
describe('UtilityBar', () => {
|
|
||||||
describe('rendering', () => {
|
|
||||||
it('renders "Contact" label', () => {
|
|
||||||
render(<UtilityBar />);
|
|
||||||
expect(screen.getByText('Contact')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders email link with correct href', () => {
|
|
||||||
render(<UtilityBar />);
|
|
||||||
const link = screen.getByRole('link', { name: 'hello@allmy.work' });
|
|
||||||
expect(link).toBeInTheDocument();
|
|
||||||
expect(link).toHaveAttribute('href', 'mailto:hello@allmy.work');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders "Download CV" button', () => {
|
|
||||||
render(<UtilityBar />);
|
|
||||||
expect(screen.getByRole('button', { name: /download cv/i })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Download CV button has primary variant class', () => {
|
|
||||||
render(<UtilityBar />);
|
|
||||||
const btn = screen.getByRole('button', { name: /download cv/i });
|
|
||||||
expect(btn).toHaveClass('bg-blue');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { CONTACT_LINKS } from '$shared/lib';
|
|
||||||
import { Button } from '$shared/ui';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fixed bottom utility bar with contact info and CV download.
|
|
||||||
*/
|
|
||||||
export function UtilityBar() {
|
|
||||||
/**
|
|
||||||
* Handles CV download action.
|
|
||||||
*/
|
|
||||||
function handleDownloadCV() {
|
|
||||||
console.log('Downloading CV...');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed bottom-0 left-0 right-0 bg-cream brutal-border-top z-40">
|
|
||||||
<div className="max-w-[2560px] mx-auto px-6 md:px-12 lg:px-16 py-4 flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<span className="text-sm uppercase tracking-wider">Contact</span>
|
|
||||||
<a href={`mailto:${CONTACT_LINKS.email}`} className="text-base hover:opacity-60 transition-opacity">
|
|
||||||
{CONTACT_LINKS.email}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<Button variant="primary" size="sm" onClick={handleDownloadCV}>
|
|
||||||
Download CV
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,2 +1 @@
|
|||||||
export * from './Footer';
|
export * from './Footer';
|
||||||
export * from './Navigation';
|
|
||||||
|
|||||||
Reference in New Issue
Block a user