feat: ExperienceCard stack field and Card subcomponent layout

This commit is contained in:
Ilia Mashkov
2026-05-18 12:39:41 +03:00
parent 543020f85c
commit 782c619a91
5 changed files with 36 additions and 13 deletions
@@ -6,6 +6,7 @@ const DEFAULT_PROPS = {
company: 'Acme Corp', company: 'Acme Corp',
period: '2021 2024', period: '2021 2024',
description: 'Built scalable frontend systems.', description: 'Built scalable frontend systems.',
stack: [],
}; };
describe('ExperienceCard', () => { describe('ExperienceCard', () => {
@@ -32,9 +33,9 @@ describe('ExperienceCard', () => {
}); });
describe('structure', () => { describe('structure', () => {
it('title is rendered as an h4', () => { it('title is rendered as an h3', () => {
render(<ExperienceCard {...DEFAULT_PROPS} />); render(<ExperienceCard {...DEFAULT_PROPS} />);
expect(screen.getByRole('heading', { level: 4 })).toHaveTextContent('Senior Developer'); expect(screen.getByRole('heading', { level: 3 })).toHaveTextContent('Senior Developer');
}); });
it('period badge has brutal-border, bg-blue, text-cream, text-sm', () => { it('period badge has brutal-border, bg-blue, text-cream, text-sm', () => {
@@ -43,11 +44,11 @@ describe('ExperienceCard', () => {
expect(badge).toHaveClass('brutal-border', 'bg-blue', 'text-cream', 'text-sm'); expect(badge).toHaveClass('brutal-border', 'bg-blue', 'text-cream', 'text-sm');
}); });
it('company paragraph has opacity-80', () => { it('company paragraph has opacity-60', () => {
render(<ExperienceCard {...DEFAULT_PROPS} />); render(<ExperienceCard {...DEFAULT_PROPS} />);
const company = screen.getByText('Acme Corp'); const company = screen.getByText('Acme Corp');
expect(company.tagName).toBe('P'); expect(company.tagName).toBe('P');
expect(company).toHaveClass('opacity-80'); expect(company).toHaveClass('opacity-60');
}); });
it('description renders via RichText with rich-text class', () => { it('description renders via RichText with rich-text class', () => {
@@ -1,4 +1,4 @@
import { Card, RichText } from '$shared/ui'; import { Card, CardContent, CardFooter, CardHeader, CardTitle, RichText } from '$shared/ui';
type Props = { type Props = {
/** /**
@@ -17,6 +17,10 @@ type Props = {
* Description of responsibilities and achievements * Description of responsibilities and achievements
*/ */
description: string; description: string;
/**
* Technologies used during this role
*/
stack: string[];
/** /**
* Additional CSS classes forwarded to the card * Additional CSS classes forwarded to the card
*/ */
@@ -24,19 +28,30 @@ type Props = {
}; };
/** /**
* Work experience card with title, company, period, and description. * Work experience card with title, company, period, description, and tech stack.
*/ */
export function ExperienceCard({ title, company, period, description, className }: Props) { export function ExperienceCard({ title, company, period, description, stack, className }: Props) {
return ( return (
<Card className={className}> <Card className={className}>
<div className="flex flex-col md:flex-row md:items-start md:justify-between mb-4 gap-4"> <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 max-w-[700px]"> <div className="flex-1">
<h4>{title}</h4> <CardTitle className="font-heading">{title}</CardTitle>
<p className="text-base opacity-80">{company}</p> <p className="text-base opacity-60">{company}</p>
</div> </div>
<span className="brutal-border px-4 py-2 bg-blue text-cream text-sm self-start">{period}</span> <span className="brutal-border px-4 py-2 bg-blue text-cream text-sm self-start">{period}</span>
</div> </CardHeader>
<RichText html={description} /> <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>
)}
</Card> </Card>
); );
} }
+4
View File
@@ -77,6 +77,10 @@ export type ExperienceRecord = BaseRecord & {
* Rich text description of responsibilities and achievements * Rich text description of responsibilities and achievements
*/ */
description: string; description: string;
/**
* Technologies used during this role
*/
stack: string[];
/** /**
* Sorting weight for chronological display * Sorting weight for chronological display
*/ */
@@ -18,6 +18,7 @@ const mockItems = [
start_date: '2022-01-01T00:00:00Z', start_date: '2022-01-01T00:00:00Z',
end_date: null, end_date: null,
description: 'Built critical systems.', description: 'Built critical systems.',
stack: ['React', 'TypeScript'],
order: 1, order: 1,
}, },
{ {
@@ -31,6 +32,7 @@ const mockItems = [
start_date: '2020-01-01T00:00:00Z', start_date: '2020-01-01T00:00:00Z',
end_date: '2021-12-31T00:00:00Z', end_date: '2021-12-31T00:00:00Z',
description: 'Learned the ropes.', description: 'Learned the ropes.',
stack: [],
order: 2, order: 2,
}, },
]; ];
@@ -21,6 +21,7 @@ export default async function ExperienceSection() {
company={exp.company} company={exp.company}
period={formatYearRange(exp.start_date, exp.end_date)} period={formatYearRange(exp.start_date, exp.end_date)}
description={exp.description} description={exp.description}
stack={exp.stack}
/> />
))} ))}
</div> </div>