Files
portfolio/src/components/ProjectCard.astro
T

119 lines
3.0 KiB
Plaintext
Raw Normal View History

---
import { Badge, Button } from '@unkn0wndo3s/nova-design-system';
import TechIcon from './TechIcon.astro';
import type { Project } from '../data/projects';
import { getLocaleFromUrl, useTranslations, localizePath } from '../i18n';
export interface Props {
project: Project;
index?: number;
headingLevel?: 'h2' | 'h3';
}
const { project, index, headingLevel: Heading = 'h3' } = Astro.props;
const locale = getLocaleFromUrl(Astro.url);
const t = useTranslations(locale);
const statusType = { live: 'success', maintained: 'info', wip: 'warning' } as const;
const statusLabel = {
live: t('work.status.live'),
maintained: t('work.status.maintained'),
wip: t('work.status.wip'),
} as const;
const href = localizePath(`/work/${project.slug}`, locale);
---
<article class="project-card">
<header class="project-card__head">
{typeof index === 'number' && (
<span class="project-card__index" aria-hidden="true">{String(index + 1).padStart(2, '0')}</span>
)}
<Badge type={statusType[project.status]}>{statusLabel[project.status]}</Badge>
</header>
<Heading class="project-card__title">
<a href={href}>{project.name}</a>
</Heading>
<p class="project-card__tagline">{project.tagline[locale]}</p>
<p class="project-card__summary">{project.summary[locale]}</p>
<ul class="project-card__stack" aria-label={t('work.stack')}>
{project.stack.map((tech) => (
<li><TechIcon name={tech.name} icon={tech.icon} size={16} /></li>
))}
</ul>
<footer class="project-card__footer">
<Button href={href} type="secondary" size="sm">{t('home.projects.view')}</Button>
</footer>
</article>
<style lang="scss">
@use '../styles/type' as type;
.project-card {
display: flex;
flex-direction: column;
gap: var(--nds-spacing-sm);
height: 100%;
padding: clamp(20px, 3vw, 32px);
background: color-mix(in srgb, var(--nds-surface) 88%, transparent);
border: var(--nds-border-width-thin) solid var(--nds-border);
border-radius: var(--nds-radius-xl);
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
&:hover,
&:focus-within {
transform: translateY(-4px);
border-color: var(--nds-border-strong);
box-shadow: var(--nds-shadow-lg);
}
}
.project-card__head {
display: flex;
align-items: center;
justify-content: space-between;
}
.project-card__index {
font-family: var(--nds-font-mono);
color: var(--nds-neutral);
}
.project-card__title {
@include type.text-2xl;
a {
color: var(--nds-text);
text-decoration: none;
&:hover { color: var(--nds-primary); }
}
}
.project-card__tagline {
@include type.text-lg;
color: var(--nds-primary);
font-weight: 500;
}
.project-card__summary {
@include type.text-base;
color: var(--nds-neutral);
flex: 1;
}
.project-card__stack {
display: flex;
flex-wrap: wrap;
gap: var(--nds-spacing-xs);
margin: 0;
padding: 0;
list-style: none;
}
.project-card__footer { margin-top: var(--nds-spacing-xs); }
</style>