fix: storybook font rendering and shared fonts module #1

Merged
ilia merged 74 commits from feat/portfolio-setup into main 2026-05-18 18:45:22 +00:00
42 changed files with 116 additions and 130 deletions
Showing only changes of commit d89dc2ee70 - Show all commits
+2
View File
@@ -20,6 +20,8 @@
# production # production
/build /build
/docs
# misc # misc
.DS_Store .DS_Store
*.pem *.pem
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { Meta, StoryObj } from '@storybook/nextjs-vite';
import { SectionAccordion } from './SectionAccordion' import { SectionAccordion } from './SectionAccordion';
const meta: Meta<typeof SectionAccordion> = { const meta: Meta<typeof SectionAccordion> = {
title: 'Shared/SectionAccordion', title: 'Shared/SectionAccordion',
@@ -11,11 +11,11 @@ const meta: Meta<typeof SectionAccordion> = {
</div> </div>
), ),
], ],
} };
export default meta export default meta;
type Story = StoryObj<typeof SectionAccordion> type Story = StoryObj<typeof SectionAccordion>;
export const Active: Story = { export const Active: Story = {
args: { args: {
@@ -24,11 +24,9 @@ export const Active: Story = {
id: 'bio', id: 'bio',
isActive: true, isActive: true,
onClick: () => {}, onClick: () => {},
children: ( children: <p>This is the expanded section content. It is visible because isActive is true.</p>,
<p>This is the expanded section content. It is visible because isActive is true.</p>
),
}, },
} };
export const Collapsed: Story = { export const Collapsed: Story = {
args: { args: {
@@ -37,8 +35,6 @@ export const Collapsed: Story = {
id: 'work', id: 'work',
isActive: false, isActive: false,
onClick: () => console.log('section clicked'), onClick: () => console.log('section clicked'),
children: ( children: <p>This content is hidden in collapsed state.</p>,
<p>This content is hidden in collapsed state.</p>
),
}, },
} };
@@ -1,7 +1,6 @@
import { describe, it, expect, vi } from 'vitest' import { render, screen } from '@testing-library/react';
import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event';
import userEvent from '@testing-library/user-event' import { SectionAccordion } from './SectionAccordion';
import { SectionAccordion } from './SectionAccordion'
const defaultProps = { const defaultProps = {
number: '01', number: '01',
@@ -10,48 +9,48 @@ const defaultProps = {
isActive: false, isActive: false,
onClick: vi.fn(), onClick: vi.fn(),
children: <p>Content here</p>, children: <p>Content here</p>,
} };
describe('SectionAccordion', () => { describe('SectionAccordion', () => {
describe('collapsed state (isActive=false)', () => { describe('collapsed state (isActive=false)', () => {
it('renders a section element with the given id', () => { it('renders a section element with the given id', () => {
const { container } = render(<SectionAccordion {...defaultProps} />) const { container } = render(<SectionAccordion {...defaultProps} />);
expect(container.querySelector('section#about')).toBeInTheDocument() expect(container.querySelector('section#about')).toBeInTheDocument();
}) });
it('renders a button with number and title', () => { it('renders a button with number and title', () => {
render(<SectionAccordion {...defaultProps} />) render(<SectionAccordion {...defaultProps} />);
expect(screen.getByRole('button', { name: /01.*About/i })).toBeInTheDocument() expect(screen.getByRole('button', { name: /01.*About/i })).toBeInTheDocument();
}) });
it('does not render children', () => { it('does not render children', () => {
render(<SectionAccordion {...defaultProps} />) render(<SectionAccordion {...defaultProps} />);
expect(screen.queryByText('Content here')).not.toBeInTheDocument() expect(screen.queryByText('Content here')).not.toBeInTheDocument();
}) });
it('calls onClick when button is clicked', async () => { it('calls onClick when button is clicked', async () => {
const onClick = vi.fn() const onClick = vi.fn();
render(<SectionAccordion {...defaultProps} onClick={onClick} />) render(<SectionAccordion {...defaultProps} onClick={onClick} />);
await userEvent.click(screen.getByRole('button')) await userEvent.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalledOnce() expect(onClick).toHaveBeenCalledOnce();
}) });
}) });
describe('active state (isActive=true)', () => { describe('active state (isActive=true)', () => {
const activeProps = { ...defaultProps, isActive: true } const activeProps = { ...defaultProps, isActive: true };
it('renders an h1 with number and title', () => { it('renders an h1 with number and title', () => {
render(<SectionAccordion {...activeProps} />) render(<SectionAccordion {...activeProps} />);
expect(screen.getByRole('heading', { level: 1, name: /01.*About/i })).toBeInTheDocument() expect(screen.getByRole('heading', { level: 1, name: /01.*About/i })).toBeInTheDocument();
}) });
it('renders children', () => { it('renders children', () => {
render(<SectionAccordion {...activeProps} />) render(<SectionAccordion {...activeProps} />);
expect(screen.getByText('Content here')).toBeInTheDocument() expect(screen.getByText('Content here')).toBeInTheDocument();
}) });
it('does not render a button', () => { it('does not render a button', () => {
render(<SectionAccordion {...activeProps} />) render(<SectionAccordion {...activeProps} />);
expect(screen.queryByRole('button')).not.toBeInTheDocument() expect(screen.queryByRole('button')).not.toBeInTheDocument();
}) });
it('content wrapper has animate-fadeIn class', () => { it('content wrapper has animate-fadeIn class', () => {
const { container } = render(<SectionAccordion {...activeProps} />) const { container } = render(<SectionAccordion {...activeProps} />);
expect(container.querySelector('.animate-fadeIn')).toBeInTheDocument() expect(container.querySelector('.animate-fadeIn')).toBeInTheDocument();
}) });
}) });
}) });
@@ -1,30 +1,30 @@
import type { ReactNode } from 'react' import type { ReactNode } from 'react';
interface SectionAccordionProps { interface SectionAccordionProps {
/** /**
* Display number prefix (e.g. "01") * Display number prefix (e.g. "01")
*/ */
number: string number: string;
/** /**
* Section title * Section title
*/ */
title: string title: string;
/** /**
* HTML id for anchor navigation * HTML id for anchor navigation
*/ */
id: string id: string;
/** /**
* Whether this section is expanded * Whether this section is expanded
*/ */
isActive: boolean isActive: boolean;
/** /**
* Called when the collapsed header is clicked * Called when the collapsed header is clicked
*/ */
onClick: () => void onClick: () => void;
/** /**
* Section content, shown when active * Section content, shown when active
*/ */
children: ReactNode children: ReactNode;
} }
/** /**
@@ -43,12 +43,11 @@ export function SectionAccordion({ number, title, id, isActive, onClick, childre
{number}. {title} {number}. {title}
</h1> </h1>
</div> </div>
<div className="animate-fadeIn"> <div className="animate-fadeIn">{children}</div>
{children}
</div>
</div> </div>
) : ( ) : (
<button <button
type="button"
onClick={onClick} onClick={onClick}
className="w-full text-left mb-3 py-3 transition-all duration-200 hover:opacity-60 group" className="w-full text-left mb-3 py-3 transition-all duration-200 hover:opacity-60 group"
> >
@@ -61,5 +60,5 @@ export function SectionAccordion({ number, title, id, isActive, onClick, childre
</button> </button>
)} )}
</section> </section>
) );
} }
@@ -1 +1 @@
export * from './SectionAccordion' export * from './SectionAccordion';
+1 -1
View File
@@ -1 +1 @@
export * from './SectionAccordion' export * from './SectionAccordion';
@@ -1,4 +1,3 @@
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { ExperienceCard } from './ExperienceCard'; import { ExperienceCard } from './ExperienceCard';
+1 -1
View File
@@ -1,2 +1,2 @@
export * from './project';
export * from './experience'; export * from './experience';
export * from './project';
+2 -2
View File
@@ -1,3 +1,3 @@
export { ProjectMetadata } from './ui/ProjectMetadata';
export { ProjectCard } from './ui/ProjectCard';
export { DetailedProjectCard } from './ui/DetailedProjectCard'; export { DetailedProjectCard } from './ui/DetailedProjectCard';
export { ProjectCard } from './ui/ProjectCard';
export { ProjectMetadata } from './ui/ProjectMetadata';
@@ -1,4 +1,3 @@
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { DetailedProjectCard } from './DetailedProjectCard'; import { DetailedProjectCard } from './DetailedProjectCard';
@@ -78,13 +77,12 @@ describe('DetailedProjectCard', () => {
it('renders image when imageUrl is provided', () => { it('renders image when imageUrl is provided', () => {
render(<DetailedProjectCard {...DEFAULT_PROPS} imageUrl="/detail.jpg" />); render(<DetailedProjectCard {...DEFAULT_PROPS} imageUrl="/detail.jpg" />);
const img = screen.getByRole('img'); expect(screen.getByRole('img')).toBeInTheDocument();
expect(img).toHaveAttribute('src', '/detail.jpg');
}); });
it('image wrapper has aspect-video and brutal-border when imageUrl is provided', () => { it('image wrapper has aspect-video and brutal-border when imageUrl is provided', () => {
const { container } = render(<DetailedProjectCard {...DEFAULT_PROPS} imageUrl="/detail.jpg" />); const { container } = render(<DetailedProjectCard {...DEFAULT_PROPS} imageUrl="/detail.jpg" />);
const imgWrapper = container.querySelector('img')!.parentElement; const imgWrapper = container.querySelector('img')?.parentElement;
expect(imgWrapper).toHaveClass('aspect-video', 'brutal-border'); expect(imgWrapper).toHaveClass('aspect-video', 'brutal-border');
}); });
}); });
@@ -1,3 +1,4 @@
import Image from 'next/image';
import { Card } from '$shared/ui'; import { Card } from '$shared/ui';
import { ProjectMetadata } from './ProjectMetadata'; import { ProjectMetadata } from './ProjectMetadata';
@@ -53,14 +54,14 @@ export function DetailedProjectCard({ title, year, role, stack, description, det
<p className="text-lg mb-6">{description}</p> <p className="text-lg mb-6">{description}</p>
{imageUrl && ( {imageUrl && (
<div className="brutal-border aspect-video bg-slate-indigo overflow-hidden"> <div className="brutal-border aspect-video bg-slate-indigo overflow-hidden relative">
<img src={imageUrl} alt={title} className="w-full h-full object-cover" /> <Image src={imageUrl} alt={title} fill className="object-cover" />
</div> </div>
)} )}
<div className="max-w-[700px] space-y-4 brutal-border-top pt-6"> <div className="max-w-[700px] space-y-4 brutal-border-top pt-6">
{details.map((detail, index) => ( {details.map((detail) => (
<p key={index} className="text-base"> <p key={detail} className="text-base">
{detail} {detail}
</p> </p>
))} ))}
+2 -4
View File
@@ -1,4 +1,3 @@
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { ProjectCard } from './ProjectCard'; import { ProjectCard } from './ProjectCard';
@@ -73,13 +72,12 @@ describe('ProjectCard', () => {
it('renders image when imageUrl is provided', () => { it('renders image when imageUrl is provided', () => {
render(<ProjectCard {...DEFAULT_PROPS} imageUrl="/project.jpg" />); render(<ProjectCard {...DEFAULT_PROPS} imageUrl="/project.jpg" />);
const img = screen.getByRole('img'); expect(screen.getByRole('img')).toBeInTheDocument();
expect(img).toHaveAttribute('src', '/project.jpg');
}); });
it('image wrapper has aspect-video and overflow-hidden when imageUrl is provided', () => { it('image wrapper has aspect-video and overflow-hidden when imageUrl is provided', () => {
const { container } = render(<ProjectCard {...DEFAULT_PROPS} imageUrl="/project.jpg" />); const { container } = render(<ProjectCard {...DEFAULT_PROPS} imageUrl="/project.jpg" />);
const imgWrapper = container.querySelector('img')!.parentElement; const imgWrapper = container.querySelector('img')?.parentElement;
expect(imgWrapper).toHaveClass('aspect-video', 'overflow-hidden', 'brutal-border'); expect(imgWrapper).toHaveClass('aspect-video', 'overflow-hidden', 'brutal-border');
}); });
}); });
+4 -3
View File
@@ -1,5 +1,6 @@
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter, Button } from '$shared/ui'; import Image from 'next/image';
import { cn } from '$shared/lib'; import { cn } from '$shared/lib';
import { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '$shared/ui';
type Props = { type Props = {
/** /**
@@ -44,8 +45,8 @@ export function ProjectCard({ title, year, description, tags, imageUrl }: Props)
</CardHeader> </CardHeader>
{imageUrl && ( {imageUrl && (
<div className="brutal-border my-6 aspect-video bg-slate-indigo overflow-hidden"> <div className="brutal-border my-6 aspect-video bg-slate-indigo overflow-hidden relative">
<img src={imageUrl} alt={title} className="w-full h-full object-cover" /> <Image src={imageUrl} alt={title} fill className="object-cover" />
</div> </div>
)} )}
@@ -1,4 +1,3 @@
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { ProjectMetadata } from './ProjectMetadata'; import { ProjectMetadata } from './ProjectMetadata';
@@ -51,19 +50,19 @@ describe('ProjectMetadata', () => {
it('year section has no brutal-border-top (first section)', () => { it('year section has no brutal-border-top (first section)', () => {
const { container } = render(<ProjectMetadata {...DEFAULT_PROPS} />); const { container } = render(<ProjectMetadata {...DEFAULT_PROPS} />);
const sections = container.firstChild!.childNodes; const sections = container.firstChild?.childNodes;
expect(sections[0]).not.toHaveClass('brutal-border-top'); expect(sections[0]).not.toHaveClass('brutal-border-top');
}); });
it('role section has brutal-border-top and pt-6', () => { it('role section has brutal-border-top and pt-6', () => {
const { container } = render(<ProjectMetadata {...DEFAULT_PROPS} />); const { container } = render(<ProjectMetadata {...DEFAULT_PROPS} />);
const sections = container.firstChild!.childNodes; const sections = container.firstChild?.childNodes;
expect(sections[1]).toHaveClass('brutal-border-top', 'pt-6'); expect(sections[1]).toHaveClass('brutal-border-top', 'pt-6');
}); });
it('stack section has brutal-border-top and pt-6', () => { it('stack section has brutal-border-top and pt-6', () => {
const { container } = render(<ProjectMetadata {...DEFAULT_PROPS} />); const { container } = render(<ProjectMetadata {...DEFAULT_PROPS} />);
const sections = container.firstChild!.childNodes; const sections = container.firstChild?.childNodes;
expect(sections[2]).toHaveClass('brutal-border-top', 'pt-6'); expect(sections[2]).toHaveClass('brutal-border-top', 'pt-6');
}); });
+1 -1
View File
@@ -1,2 +1,2 @@
export * from './types';
export * from './client'; export * from './client';
export * from './types';
+6 -7
View File
@@ -21,14 +21,13 @@ export type BaseRecord = {
/** /**
* Record last update timestamp (ISO 8601) * Record last update timestamp (ISO 8601)
*/ */
updated: string; updated: string;
}; };
/**
* PocketBase collection for simple text blocks (Intro, Bio).
*/
export type PageContentRecord = BaseRecord & {
/**
* PocketBase collection for simple text blocks (Intro, Bio).
*/
export type PageContentRecord = BaseRecord & {
/** /**
* Slug corresponding to the parent section * Slug corresponding to the parent section
*/ */
+2 -2
View File
@@ -1,3 +1,3 @@
export * from './ui';
export * from './lib';
export * from './api'; export * from './api';
export * from './lib';
export * from './ui';
-1
View File
@@ -1,4 +1,3 @@
import { describe, it, expect } from 'vitest';
import { cn } from './cn'; import { cn } from './cn';
describe('cn', () => { describe('cn', () => {
+1 -1
View File
@@ -1,4 +1,4 @@
import { clsx, type ClassValue } from 'clsx'; import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
/** /**
-1
View File
@@ -1,4 +1,3 @@
import { describe, expect, it } from 'vitest';
import { formatYearRange } from './formatDate'; import { formatYearRange } from './formatDate';
describe('formatYearRange', () => { describe('formatYearRange', () => {
+1 -1
View File
@@ -1,2 +1,2 @@
export { Badge } from './ui/Badge';
export type { BadgeVariant } from './ui/Badge'; export type { BadgeVariant } from './ui/Badge';
export { Badge } from './ui/Badge';
-1
View File
@@ -1,4 +1,3 @@
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { Badge } from './Badge'; import { Badge } from './Badge';
+1 -1
View File
@@ -1,2 +1,2 @@
export type { ButtonSize, ButtonVariant } from './ui/Button';
export { Button } from './ui/Button'; export { Button } from './ui/Button';
export type { ButtonVariant, ButtonSize } from './ui/Button';
-1
View File
@@ -1,4 +1,3 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { Button } from './Button'; import { Button } from './Button';
+1 -1
View File
@@ -1,2 +1,2 @@
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './ui/Card';
export type { CardBackground } from './ui/Card'; export type { CardBackground } from './ui/Card';
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './ui/Card';
+1 -1
View File
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'; import type { Meta, StoryObj } from '@storybook/nextjs-vite';
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './Card'; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './Card';
const meta: Meta<typeof Card> = { const meta: Meta<typeof Card> = {
title: 'Shared/Card', title: 'Shared/Card',
+1 -2
View File
@@ -1,6 +1,5 @@
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './Card'; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './Card';
describe('Card', () => { describe('Card', () => {
describe('rendering', () => { describe('rendering', () => {
+2 -3
View File
@@ -1,4 +1,3 @@
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { Input, Textarea } from './Input'; import { Input, Textarea } from './Input';
@@ -35,7 +34,7 @@ describe('Input', () => {
const input = screen.getByRole('textbox'); const input = screen.getByRole('textbox');
const errorId = input.getAttribute('aria-describedby'); const errorId = input.getAttribute('aria-describedby');
expect(errorId).toBeTruthy(); expect(errorId).toBeTruthy();
expect(document.getElementById(errorId!)).toHaveTextContent('Required'); expect(document.getElementById(errorId as string)).toHaveTextContent('Required');
}); });
it('no aria-describedby when no error', () => { it('no aria-describedby when no error', () => {
render(<Input />); render(<Input />);
@@ -100,7 +99,7 @@ describe('Textarea', () => {
const textarea = screen.getByRole('textbox'); const textarea = screen.getByRole('textbox');
const errorId = textarea.getAttribute('aria-describedby'); const errorId = textarea.getAttribute('aria-describedby');
expect(errorId).toBeTruthy(); expect(errorId).toBeTruthy();
expect(document.getElementById(errorId!)).toHaveTextContent('Too short'); expect(document.getElementById(errorId as string)).toHaveTextContent('Too short');
}); });
it('no aria-describedby when no error', () => { it('no aria-describedby when no error', () => {
render(<Textarea />); render(<Textarea />);
+1 -1
View File
@@ -1,4 +1,4 @@
import { useId, type InputHTMLAttributes, type TextareaHTMLAttributes } from 'react'; import { type InputHTMLAttributes, type TextareaHTMLAttributes, useId } from 'react';
import { cn } from '$shared/lib'; import { cn } from '$shared/lib';
interface InputProps extends InputHTMLAttributes<HTMLInputElement> { interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
+2 -2
View File
@@ -1,2 +1,2 @@
export { Section, Container } from './ui/Section'; export type { ContainerSize, SectionBackground } from './ui/Section';
export type { SectionBackground, ContainerSize } from './ui/Section'; export { Container, Section } from './ui/Section';
+1 -1
View File
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'; import type { Meta, StoryObj } from '@storybook/nextjs-vite';
import { Section, Container } from './Section'; import { Container, Section } from './Section';
const meta: Meta<typeof Section> = { const meta: Meta<typeof Section> = {
title: 'Shared/Section', title: 'Shared/Section',
+3 -4
View File
@@ -1,6 +1,5 @@
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { Section, Container } from './Section'; import { Container, Section } from './Section';
describe('Section', () => { describe('Section', () => {
describe('rendering', () => { describe('rendering', () => {
@@ -36,13 +35,13 @@ describe('Section', () => {
describe('bordered', () => { describe('bordered', () => {
it('no border classes by default', () => { it('no border classes by default', () => {
const { container } = render(<Section>x</Section>); const { container } = render(<Section>x</Section>);
const el = container.querySelector('section')!; const el = container.querySelector('section') as HTMLElement;
expect(el).not.toHaveClass('brutal-border-top'); expect(el).not.toHaveClass('brutal-border-top');
expect(el).not.toHaveClass('brutal-border-bottom'); expect(el).not.toHaveClass('brutal-border-bottom');
}); });
it('adds top and bottom borders when bordered=true', () => { it('adds top and bottom borders when bordered=true', () => {
const { container } = render(<Section bordered>x</Section>); const { container } = render(<Section bordered>x</Section>);
const el = container.querySelector('section')!; const el = container.querySelector('section') as HTMLElement;
expect(el).toHaveClass('brutal-border-top'); expect(el).toHaveClass('brutal-border-top');
expect(el).toHaveClass('brutal-border-bottom'); expect(el).toHaveClass('brutal-border-bottom');
}); });
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'; import type { Meta, StoryObj } from '@storybook/nextjs-vite';
import { TechStackGrid, TechStackBrick } from './TechStack'; import { TechStackBrick, TechStackGrid } from './TechStack';
const meta: Meta<typeof TechStackGrid> = { const meta: Meta<typeof TechStackGrid> = {
title: 'Shared/TechStack', title: 'Shared/TechStack',
@@ -1,4 +1,3 @@
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { TechStackBrick, TechStackGrid } from './TechStack'; import { TechStackBrick, TechStackGrid } from './TechStack';
@@ -41,11 +40,11 @@ describe('TechStackGrid', () => {
}); });
it('renders correct number of bricks', () => { it('renders correct number of bricks', () => {
const { container } = render(<TechStackGrid skills={['A', 'B', 'C']} />); const { container } = render(<TechStackGrid skills={['A', 'B', 'C']} />);
expect(container.firstChild!.childNodes).toHaveLength(3); expect(container.firstChild?.childNodes).toHaveLength(3);
}); });
it('renders empty grid with no skills', () => { it('renders empty grid with no skills', () => {
const { container } = render(<TechStackGrid skills={[]} />); const { container } = render(<TechStackGrid skills={[]} />);
expect(container.firstChild!.childNodes).toHaveLength(0); expect(container.firstChild?.childNodes).toHaveLength(0);
}); });
}); });
+2 -2
View File
@@ -47,8 +47,8 @@ export function TechStackGrid({ skills, className }: TechStackGridProps) {
<div <div
className={cn('grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4', className)} className={cn('grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4', className)}
> >
{skills.map((skill, index) => ( {skills.map((skill) => (
<TechStackBrick key={index} name={skill} /> <TechStackBrick key={skill} name={skill} />
))} ))}
</div> </div>
); );
+1 -1
View File
@@ -1,4 +1,4 @@
export type { NavItem } from './model/types';
export { MobileNav } from './ui/MobileNav'; export { MobileNav } from './ui/MobileNav';
export { SidebarNav } from './ui/SidebarNav'; export { SidebarNav } from './ui/SidebarNav';
export { UtilityBar } from './ui/UtilityBar'; export { UtilityBar } from './ui/UtilityBar';
export type { NavItem } from './model/types';
+2 -3
View File
@@ -1,8 +1,7 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { MobileNav } from './MobileNav';
import type { NavItem } from '../model/types'; import type { NavItem } from '../model/types';
import { MobileNav } from './MobileNav';
const ITEMS: NavItem[] = [{ id: 'about', label: 'About', number: '01' }]; const ITEMS: NavItem[] = [{ id: 'about', label: 'About', number: '01' }];
@@ -38,7 +37,7 @@ describe('MobileNav', () => {
// item button label contains number + label text; find by accessible name fragment // item button label contains number + label text; find by accessible name fragment
const itemBtn = screen.getAllByRole('button').find((b) => b.textContent?.includes('About')); const itemBtn = screen.getAllByRole('button').find((b) => b.textContent?.includes('About'));
expect(itemBtn).toBeDefined(); expect(itemBtn).toBeDefined();
await userEvent.click(itemBtn!); await userEvent.click(itemBtn as HTMLElement);
expect(screen.queryByText('Close')).not.toBeInTheDocument(); expect(screen.queryByText('Close')).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Menu' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Menu' })).toBeInTheDocument();
}); });
+2
View File
@@ -34,6 +34,7 @@ export function MobileNav({ items }: Props) {
<div className="px-6 py-4 flex items-center justify-between"> <div className="px-6 py-4 flex items-center justify-between">
<h4>allmy.work</h4> <h4>allmy.work</h4>
<button <button
type="button"
onClick={() => setIsOpen((prev) => !prev)} onClick={() => setIsOpen((prev) => !prev)}
className="brutal-border px-4 py-2 bg-carbon-black text-ochre-clay" className="brutal-border px-4 py-2 bg-carbon-black text-ochre-clay"
> >
@@ -44,6 +45,7 @@ export function MobileNav({ items }: Props) {
<div className="px-6 py-6 brutal-border-top space-y-2 max-h-[80vh] overflow-y-auto"> <div className="px-6 py-6 brutal-border-top space-y-2 max-h-[80vh] overflow-y-auto">
{items.map((item) => ( {items.map((item) => (
<button <button
type="button"
key={item.id} key={item.id}
onClick={() => scrollToSection(item.id)} onClick={() => scrollToSection(item.id)}
className="w-full text-left brutal-border bg-ochre-clay px-4 py-3" className="w-full text-left brutal-border bg-ochre-clay px-4 py-3"
@@ -1,5 +1,4 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { NavItem } from '../model/types'; import type { NavItem } from '../model/types';
import { SidebarNav } from './SidebarNav'; import { SidebarNav } from './SidebarNav';
+5 -2
View File
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useState, useEffect } from 'react'; import { useEffect, useState } from 'react';
import { cn } from '$shared/lib'; import { cn } from '$shared/lib';
import type { NavItem } from '../model/types'; import type { NavItem } from '../model/types';
@@ -31,7 +31,9 @@ export function SidebarNav({ items }: Props) {
items.forEach((item) => { items.forEach((item) => {
const el = document.getElementById(item.id); const el = document.getElementById(item.id);
if (el) observer.observe(el); if (el) {
observer.observe(el);
}
}); });
return () => observer.disconnect(); return () => observer.disconnect();
@@ -62,6 +64,7 @@ export function SidebarNav({ items }: Props) {
const isActive = activeSection === item.id; const isActive = activeSection === item.id;
return ( return (
<button <button
type="button"
key={item.id} key={item.id}
onClick={() => scrollToSection(item.id)} onClick={() => scrollToSection(item.id)}
className={cn( className={cn(
@@ -1,4 +1,3 @@
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { UtilityBar } from './UtilityBar'; import { UtilityBar } from './UtilityBar';
+1
View File
@@ -18,6 +18,7 @@
"name": "next" "name": "next"
} }
], ],
"types": ["vitest/globals"],
"paths": { "paths": {
"@/*": ["./*"], "@/*": ["./*"],
"$shared/*": ["./src/shared/*"], "$shared/*": ["./src/shared/*"],