refactor: group project/ui components into subdirectories
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
||||
import { ProjectMetadata } from './ProjectMetadata';
|
||||
|
||||
const meta: Meta<typeof ProjectMetadata> = {
|
||||
title: 'Entities/ProjectMetadata',
|
||||
component: ProjectMetadata,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="p-8 bg-ochre-clay max-w-xs">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof ProjectMetadata>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
year: '2024',
|
||||
role: 'Lead Frontend Engineer',
|
||||
stack: ['React', 'TypeScript', 'Next.js', 'Tailwind'],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ProjectMetadata } from './ProjectMetadata';
|
||||
|
||||
const DEFAULT_PROPS = {
|
||||
year: '2024',
|
||||
role: 'Frontend Engineer',
|
||||
stack: ['React', 'TypeScript', 'Tailwind'],
|
||||
};
|
||||
|
||||
describe('ProjectMetadata', () => {
|
||||
describe('rendering', () => {
|
||||
it('renders the year value', () => {
|
||||
render(<ProjectMetadata {...DEFAULT_PROPS} />);
|
||||
expect(screen.getByText('2024')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the YEAR label', () => {
|
||||
render(<ProjectMetadata {...DEFAULT_PROPS} />);
|
||||
expect(screen.getByText('YEAR')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the role value', () => {
|
||||
render(<ProjectMetadata {...DEFAULT_PROPS} />);
|
||||
expect(screen.getByText('Frontend Engineer')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the ROLE label', () => {
|
||||
render(<ProjectMetadata {...DEFAULT_PROPS} />);
|
||||
expect(screen.getByText('ROLE')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the STACK label', () => {
|
||||
render(<ProjectMetadata {...DEFAULT_PROPS} />);
|
||||
expect(screen.getByText('STACK')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders each stack technology', () => {
|
||||
render(<ProjectMetadata {...DEFAULT_PROPS} />);
|
||||
expect(screen.getByText('React')).toBeInTheDocument();
|
||||
expect(screen.getByText('TypeScript')).toBeInTheDocument();
|
||||
expect(screen.getByText('Tailwind')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('structure', () => {
|
||||
it('outer div has space-y-6', () => {
|
||||
const { container } = render(<ProjectMetadata {...DEFAULT_PROPS} />);
|
||||
expect(container.firstChild).toHaveClass('space-y-6');
|
||||
});
|
||||
|
||||
it('year section has no brutal-border-top (first section)', () => {
|
||||
const { container } = render(<ProjectMetadata {...DEFAULT_PROPS} />);
|
||||
const sections = container.firstChild?.childNodes as NodeListOf<Element>;
|
||||
expect(sections[0]).not.toHaveClass('brutal-border-top');
|
||||
});
|
||||
|
||||
it('role section has brutal-border-top and pt-6', () => {
|
||||
const { container } = render(<ProjectMetadata {...DEFAULT_PROPS} />);
|
||||
const sections = container.firstChild?.childNodes as NodeListOf<Element>;
|
||||
expect(sections[1]).toHaveClass('brutal-border-top', 'pt-6');
|
||||
});
|
||||
|
||||
it('stack section has brutal-border-top and pt-6', () => {
|
||||
const { container } = render(<ProjectMetadata {...DEFAULT_PROPS} />);
|
||||
const sections = container.firstChild?.childNodes as NodeListOf<Element>;
|
||||
expect(sections[2]).toHaveClass('brutal-border-top', 'pt-6');
|
||||
});
|
||||
|
||||
it('label has text-xs uppercase tracking-wider opacity-60', () => {
|
||||
render(<ProjectMetadata {...DEFAULT_PROPS} />);
|
||||
const yearLabel = screen.getByText('YEAR');
|
||||
expect(yearLabel).toHaveClass('text-xs', 'uppercase', 'tracking-wider', 'opacity-60');
|
||||
});
|
||||
|
||||
it('year value has text-base font-bold', () => {
|
||||
render(<ProjectMetadata {...DEFAULT_PROPS} />);
|
||||
const yearValue = screen.getByText('2024');
|
||||
expect(yearValue).toHaveClass('text-base', 'font-bold');
|
||||
});
|
||||
|
||||
it('each stack tech is rendered as a <p> with text-sm', () => {
|
||||
render(<ProjectMetadata {...DEFAULT_PROPS} />);
|
||||
const techEl = screen.getByText('React');
|
||||
expect(techEl.tagName).toBe('P');
|
||||
expect(techEl).toHaveClass('text-sm');
|
||||
});
|
||||
});
|
||||
|
||||
describe('className passthrough', () => {
|
||||
it('merges custom className onto outer div', () => {
|
||||
const { container } = render(<ProjectMetadata {...DEFAULT_PROPS} className="my-custom" />);
|
||||
expect(container.firstChild).toHaveClass('my-custom');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { cn } from '$shared/lib';
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* Project year
|
||||
*/
|
||||
year: string;
|
||||
/**
|
||||
* Developer role on the project
|
||||
*/
|
||||
role: string;
|
||||
/**
|
||||
* Technology stack list
|
||||
*/
|
||||
stack: string[];
|
||||
/**
|
||||
* Additional CSS classes
|
||||
*/
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sidebar metadata display for a project: year, role, and stack.
|
||||
*/
|
||||
export function ProjectMetadata({ year, role, stack, className }: Props) {
|
||||
return (
|
||||
<div className={cn('space-y-6', className)}>
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-wider opacity-60">YEAR</p>
|
||||
<p className="text-base font-bold">{year}</p>
|
||||
</div>
|
||||
<div className="brutal-border-top pt-6">
|
||||
<p className="text-xs uppercase tracking-wider opacity-60">ROLE</p>
|
||||
<p className="text-base font-bold">{role}</p>
|
||||
</div>
|
||||
<div className="brutal-border-top pt-6">
|
||||
<p className="text-xs uppercase tracking-wider opacity-60">STACK</p>
|
||||
{stack.map((tech) => (
|
||||
<p key={tech} className="text-sm">
|
||||
{tech}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user