feat: ProjectCard — add url prop, RichText description, open link in new tab
This commit is contained in:
@@ -6,6 +6,7 @@ const DEFAULT_PROPS = {
|
|||||||
year: '2024',
|
year: '2024',
|
||||||
description: 'A cool project description',
|
description: 'A cool project description',
|
||||||
tags: ['React', 'Node'],
|
tags: ['React', 'Node'],
|
||||||
|
url: 'https://example.com',
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('ProjectCard', () => {
|
describe('ProjectCard', () => {
|
||||||
@@ -33,7 +34,17 @@ describe('ProjectCard', () => {
|
|||||||
|
|
||||||
it('renders the View Project button', () => {
|
it('renders the View Project button', () => {
|
||||||
render(<ProjectCard {...DEFAULT_PROPS} />);
|
render(<ProjectCard {...DEFAULT_PROPS} />);
|
||||||
expect(screen.getByRole('button', { name: /view project/i })).toBeInTheDocument();
|
expect(screen.getByRole('link', { name: /view project/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('View Project link points to the project url', () => {
|
||||||
|
render(<ProjectCard {...DEFAULT_PROPS} />);
|
||||||
|
expect(screen.getByRole('link', { name: /view project/i })).toHaveAttribute('href', 'https://example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('View Project link opens in a new tab', () => {
|
||||||
|
render(<ProjectCard {...DEFAULT_PROPS} />);
|
||||||
|
expect(screen.getByRole('link', { name: /view project/i })).toHaveAttribute('target', '_blank');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -51,7 +62,7 @@ describe('ProjectCard', () => {
|
|||||||
|
|
||||||
it('View Project button is inside the sidebar column', () => {
|
it('View Project button is inside the sidebar column', () => {
|
||||||
render(<ProjectCard {...DEFAULT_PROPS} />);
|
render(<ProjectCard {...DEFAULT_PROPS} />);
|
||||||
const btn = screen.getByRole('button', { name: /view project/i });
|
const btn = screen.getByRole('link', { name: /view project/i });
|
||||||
expect(btn.closest('.brutal-border-sidebar')).toBeInTheDocument();
|
expect(btn.closest('.brutal-border-sidebar')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -86,7 +97,7 @@ describe('ProjectCard', () => {
|
|||||||
|
|
||||||
it('View Project button uses sm size', () => {
|
it('View Project button uses sm size', () => {
|
||||||
render(<ProjectCard {...DEFAULT_PROPS} />);
|
render(<ProjectCard {...DEFAULT_PROPS} />);
|
||||||
const btn = screen.getByRole('button', { name: /view project/i });
|
const btn = screen.getByRole('link', { name: /view project/i });
|
||||||
expect(btn).toHaveClass('px-4', 'py-2', 'text-sm');
|
expect(btn).toHaveClass('px-4', 'py-2', 'text-sm');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { cn } from '$shared/lib';
|
import { cn } from '$shared/lib';
|
||||||
import { Badge, Button, Card, CardSidebar, CardTitle } from '$shared/ui';
|
import { Badge, Button, Card, CardSidebar, CardTitle, RichText } from '$shared/ui';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
/**
|
/**
|
||||||
@@ -19,6 +19,10 @@ type Props = {
|
|||||||
* Technology or category tags
|
* Technology or category tags
|
||||||
*/
|
*/
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
/**
|
||||||
|
* Project's URL
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
/**
|
/**
|
||||||
* Optional preview image URL
|
* Optional preview image URL
|
||||||
*/
|
*/
|
||||||
@@ -30,7 +34,7 @@ type Props = {
|
|||||||
* Sidebar: year badge, stack tags, View Project button.
|
* Sidebar: year badge, stack tags, View Project button.
|
||||||
* Main: title, optional image, description.
|
* Main: title, optional image, description.
|
||||||
*/
|
*/
|
||||||
export function ProjectCard({ title, year, description, tags, imageUrl }: Props) {
|
export function ProjectCard({ title, year, description, tags, url, imageUrl }: Props) {
|
||||||
return (
|
return (
|
||||||
<Card className={cn('group hover:shadow-brutal-xl transition-shadow duration-300')}>
|
<Card className={cn('group hover:shadow-brutal-xl transition-shadow duration-300')}>
|
||||||
<CardSidebar
|
<CardSidebar
|
||||||
@@ -46,7 +50,7 @@ export function ProjectCard({ title, year, description, tags, imageUrl }: Props)
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Button variant="primary" size="sm" className="w-full">
|
<Button href={url} variant="primary" size="sm" className="w-full">
|
||||||
View Project
|
View Project
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,7 +63,7 @@ export function ProjectCard({ title, year, description, tags, imageUrl }: Props)
|
|||||||
<Image src={imageUrl} alt={title} fill className="object-cover" />
|
<Image src={imageUrl} alt={title} fill className="object-cover" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<p className="opacity-80">{description}</p>
|
<RichText html={description} />
|
||||||
</div>
|
</div>
|
||||||
</CardSidebar>
|
</CardSidebar>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user