feat: CardSidebar layout component and ExperienceCard sidebar redesign

Sidebar: period badge, company, stack tags.
Main: role title and rich-text description.
This commit is contained in:
Ilia Mashkov
2026-05-18 12:51:33 +03:00
parent 782c619a91
commit 1550989fd9
7 changed files with 159 additions and 30 deletions
@@ -32,6 +32,32 @@ describe('ExperienceCard', () => {
});
});
describe('layout', () => {
it('period badge is inside the sidebar column', () => {
render(<ExperienceCard {...DEFAULT_PROPS} />);
const badge = screen.getByText('2021 2024');
expect(badge.closest('.brutal-border-sidebar')).toBeInTheDocument();
});
it('company name is inside the sidebar column', () => {
render(<ExperienceCard {...DEFAULT_PROPS} />);
const company = screen.getByText('Acme Corp');
expect(company.closest('.brutal-border-sidebar')).toBeInTheDocument();
});
it('title is outside the sidebar column', () => {
render(<ExperienceCard {...DEFAULT_PROPS} />);
const title = screen.getByText('Senior Developer');
expect(title.closest('.brutal-border-sidebar')).toBeNull();
});
it('description is outside the sidebar column', () => {
render(<ExperienceCard {...DEFAULT_PROPS} />);
const desc = screen.getByText('Built scalable frontend systems.');
expect(desc.closest('.brutal-border-sidebar')).toBeNull();
});
});
describe('structure', () => {
it('title is rendered as an h3', () => {
render(<ExperienceCard {...DEFAULT_PROPS} />);
@@ -44,25 +70,33 @@ describe('ExperienceCard', () => {
expect(badge).toHaveClass('brutal-border', 'bg-blue', 'text-cream', 'text-sm');
});
it('company paragraph has opacity-60', () => {
render(<ExperienceCard {...DEFAULT_PROPS} />);
const company = screen.getByText('Acme Corp');
expect(company.tagName).toBe('P');
expect(company).toHaveClass('opacity-60');
});
it('description renders via RichText with rich-text class', () => {
render(<ExperienceCard {...DEFAULT_PROPS} />);
const desc = screen.getByText('Built scalable frontend systems.');
expect(desc.closest('.rich-text')).toBeInTheDocument();
});
it('card has brutal-border class (from Card component)', () => {
it('card has brutal-border class', () => {
const { container } = render(<ExperienceCard {...DEFAULT_PROPS} />);
expect(container.firstChild).toHaveClass('brutal-border');
});
});
describe('stack tags', () => {
it('renders stack tags in the sidebar', () => {
render(<ExperienceCard {...DEFAULT_PROPS} stack={['React', 'TypeScript']} />);
const react = screen.getByText('React');
const ts = screen.getByText('TypeScript');
expect(react.closest('.brutal-border-sidebar')).toBeInTheDocument();
expect(ts.closest('.brutal-border-sidebar')).toBeInTheDocument();
});
it('renders nothing extra when stack is empty', () => {
render(<ExperienceCard {...DEFAULT_PROPS} stack={[]} />);
expect(screen.queryByRole('list')).toBeNull();
});
});
describe('className passthrough', () => {
it('forwards className to the card', () => {
const { container } = render(<ExperienceCard {...DEFAULT_PROPS} className="custom-class" />);
@@ -1,4 +1,4 @@
import { Card, CardContent, CardFooter, CardHeader, CardTitle, RichText } from '$shared/ui';
import { Card, CardSidebar, CardTitle, RichText } from '$shared/ui';
type Props = {
/**
@@ -28,30 +28,38 @@ type Props = {
};
/**
* Work experience card with title, company, period, description, and tech stack.
* Work experience card with sidebar layout.
* Sidebar: period badge, company, stack tags.
* Main: job title and rich-text description.
*/
export function ExperienceCard({ title, company, period, description, stack, className }: Props) {
return (
<Card className={className}>
<CardHeader className="flex flex-col md:flex-row md:items-start md:justify-between gap-4 pb-6 md:pb-8 brutal-border-bottom">
<div className="flex-1">
<CardSidebar
sidebar={
<div className="flex flex-col gap-4">
<span className="brutal-border px-4 py-2 bg-blue text-cream text-sm self-start">{period}</span>
<p className="text-base font-medium">{company}</p>
{stack.length > 0 && (
<div className="flex flex-wrap gap-2">
{stack.map((tech) => (
<span
key={tech}
className="brutal-border px-3 py-1 bg-cream text-blue text-sm uppercase tracking-wide"
>
{tech}
</span>
))}
</div>
)}
</div>
}
>
<div className="flex flex-col gap-4">
<CardTitle className="font-heading">{title}</CardTitle>
<p className="text-base opacity-60">{company}</p>
<RichText html={description} />
</div>
<span className="brutal-border px-4 py-2 bg-blue text-cream text-sm self-start">{period}</span>
</CardHeader>
<CardContent>
<RichText html={description} />
</CardContent>
{stack.length > 0 && (
<CardFooter className="flex flex-wrap gap-2">
{stack.map((tech) => (
<span key={tech} className="brutal-border px-3 py-1 bg-cream text-blue text-sm uppercase tracking-wide">
{tech}
</span>
))}
</CardFooter>
)}
</CardSidebar>
</Card>
);
}