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 './Navigation';
|
||||
|
||||
Reference in New Issue
Block a user