Create projects section (#99)
This commit is contained in:
parent
1c8c93e3c4
commit
da43c079b0
5 changed files with 88 additions and 63 deletions
|
|
@ -9,74 +9,82 @@ import Typography from '@/atoms/typography.astro';
|
||||||
import type { I18n } from '@/types/i18n';
|
import type { I18n } from '@/types/i18n';
|
||||||
import type { Project } from '@/types/portfolio-section';
|
import type { Project } from '@/types/portfolio-section';
|
||||||
|
|
||||||
export interface Props extends astroHTML.JSX.HTMLAttributes {
|
export interface Props {
|
||||||
value: Project;
|
project: Project;
|
||||||
i18n: I18n;
|
i18n: I18n;
|
||||||
}
|
}
|
||||||
const { value, i18n, ...props } = Astro.props;
|
const { project, i18n } = Astro.props;
|
||||||
const ProjectTimelineItem = 'div';
|
const { description, details, endDate, name, socials, startDate, tags, image } = project;
|
||||||
|
|
||||||
const setLabelValue = (val: string | string[]) =>
|
// Alt has to destructured separately, because otherwise eslint complains about
|
||||||
Array.isArray(val) ? val.map((v, id) => (id !== val.length - 1 ? v.concat(', ') : v)) : val;
|
// the missing alt attribute on the Image component.
|
||||||
|
const { alt, ...sharedImageProps } = {
|
||||||
|
src: image,
|
||||||
|
aspectRatio: '1/1',
|
||||||
|
alt: `Thumbnail for ${name} project`,
|
||||||
|
format: 'webp',
|
||||||
|
} as const;
|
||||||
---
|
---
|
||||||
|
|
||||||
<ProjectTimelineItem class:list={[props.className]}>
|
<div class:list={['flex', 'flex-col', 'gap-6', 'py-8', 'px-4']}>
|
||||||
<div class:list={['flex', 'flex-col', 'sm:grid', 'overflow-hidden', 'grid-cols-[120px_minmax(200px,_1fr)]', 'gap-2']}>
|
<div class:list={['flex', 'flex-col', 'gap-4']}>
|
||||||
|
<div class:list={['flex', 'gap-6']}>
|
||||||
<Image
|
<Image
|
||||||
class:list={['rounded-lg', 'object-cover', 'max-w-[120px]', 'm-0', 'overflow-hidden', 'sm:block', 'hidden']}
|
class:list={['rounded-lg', 'object-cover', 'max-w-[120px]', 'overflow-hidden', 'sm:block', 'hidden']}
|
||||||
src={value.image}
|
{...sharedImageProps}
|
||||||
aspectRatio="1/1"
|
alt={alt}
|
||||||
alt={''}
|
|
||||||
format="webp"
|
|
||||||
/>
|
/>
|
||||||
<div class:list={['col-start-2', 'col-span-2', 'sm:mx-6']}>
|
<div class:list={['flex', 'flex-col', 'gap-4', 'w-full']}>
|
||||||
<div class:list={['flex', 'justify-between']}>
|
<div class:list={['flex', 'justify-between']}>
|
||||||
<Typography variant="item-title">{value.name}</Typography>
|
<div>
|
||||||
<div class:list={['fixed', 'top-3', 'right-3', 'md:flex', 'md:flex-wrap', 'gap-3', '[&>a]:my-2']}>
|
<Typography variant="item-title">{name}</Typography>
|
||||||
{value.socials?.map(({ icon, url }) => <IconButton icon={icon} href={url} target="_blank" size="small" />)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Timestamp
|
<Timestamp
|
||||||
startDate={value.startDate}
|
startDate={startDate}
|
||||||
endDate={value.endDate}
|
endDate={endDate}
|
||||||
locale={i18n.locale}
|
locale={i18n.locale}
|
||||||
translationForNow={i18n.translations.now}
|
translationForNow={i18n.translations.now}
|
||||||
/>
|
/>
|
||||||
<Image
|
|
||||||
class:list={['rounded-lg', 'object-cover', 'my-2', 'max-w-[120px]', 'sm:block', 'sm:hidden']}
|
|
||||||
src={value.image}
|
|
||||||
aspectRatio="1/1"
|
|
||||||
alt={''}
|
|
||||||
format="webp"
|
|
||||||
/>
|
|
||||||
<div class:list={['flex', 'md:gap-3', 'md:flex-row', 'flex-col', 'my-4']}>
|
|
||||||
<div class:list={['md:w-2/6']}>
|
|
||||||
{
|
|
||||||
value.details
|
|
||||||
.slice(0, Math.round(value.details.length / 2))
|
|
||||||
.map((d) => <LabelledValue label={d.label} value={setLabelValue(d.value)} />)
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class:list={['flex', 'gap-2']}>
|
||||||
|
{socials?.map(({ icon, url }) => <IconButton icon={icon} href={url} target="_blank" size="small" />)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Image
|
||||||
|
class:list={['rounded-lg', 'object-cover', 'max-w-[120px]', 'sm:block', 'sm:hidden']}
|
||||||
|
{...sharedImageProps}
|
||||||
|
alt={alt}
|
||||||
|
/>
|
||||||
|
<div class:list={['inline-grid', 'xl:grid-cols-[auto_auto]', 'w-full']}>
|
||||||
{
|
{
|
||||||
value.details
|
details.map(({ label: detailLabel, value: detailValue }) => (
|
||||||
.slice(Math.round(value.details.length / 2))
|
<LabelledValue
|
||||||
.map((d) => <LabelledValue label={d.label} value={setLabelValue(d.value)} />)
|
label={detailLabel}
|
||||||
|
value={typeof detailValue === 'object' ? detailValue.join(', ') : detailValue}
|
||||||
|
/>
|
||||||
|
))
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class:list={['col-start-1 col-span-3']}>
|
<div class:list={['col-start-1 col-span-3']}>
|
||||||
<Typography variant="paragraph">{value.description}</Typography>
|
<Typography variant="paragraph">{description}</Typography>
|
||||||
</div>
|
</div>
|
||||||
<div class:list={['flex', 'gap-3', 'flex-wrap', 'sm:flex-nowrap', 'mt-6']}>
|
</div>
|
||||||
|
<div class:list={['flex', 'gap-3', 'flex-wrap']}>
|
||||||
{
|
{
|
||||||
value.tags.map((t) => (
|
tags.map(({ name: tagName, icon, iconColor, url }) => {
|
||||||
<Tag name={t.icon} color={t.iconColor}>
|
return url ? (
|
||||||
{t.name}
|
<a href={url} target="_blank" rel="noopener noreferrer">
|
||||||
|
<Tag name={icon} color={iconColor}>
|
||||||
|
{tagName}
|
||||||
</Tag>
|
</Tag>
|
||||||
))
|
</a>
|
||||||
|
) : (
|
||||||
|
<Tag name={icon} color={iconColor}>
|
||||||
|
{tagName}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ProjectTimelineItem>
|
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,13 @@ const WorkTimelineItem = 'div';
|
||||||
---
|
---
|
||||||
|
|
||||||
<WorkTimelineItem class:list={['flex', 'flex-col', 'gap-2', 'md:gap-0', 'mb-4', props.class]}>
|
<WorkTimelineItem class:list={['flex', 'flex-col', 'gap-2', 'md:gap-0', 'mb-4', props.class]}>
|
||||||
<div class:list={['flex', 'justify-between', 'items-start', 'gap-4', 'sm:items-center']}>
|
<div class:list={['flex', 'flex-row', 'justify-between', 'w-full']}>
|
||||||
|
<div>
|
||||||
<Typography variant="item-title"
|
<Typography variant="item-title"
|
||||||
>{job.role} <span class="font-medium"> — {job.company}</span>
|
>{job.role} <span class="font-medium"> — {job.company}</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
<div class:list={['md:flex', 'sm:flex-wrap', 'gap-3', '[&>a]:mb-2', 'md:[&>a]:my-2']}>
|
</div>
|
||||||
|
<div class:list={['flex', 'flex-wrap', 'gap-2']}>
|
||||||
{job.socials?.map(({ icon, url }) => <IconButton icon={icon} href={url} target="_blank" size="small" />)}
|
{job.socials?.map(({ icon, url }) => <IconButton icon={icon} href={url} target="_blank" size="small" />)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,20 @@
|
||||||
---
|
---
|
||||||
|
import Divider from '@/atoms/divider.astro';
|
||||||
import SectionCard from '@/atoms/section-card.astro';
|
import SectionCard from '@/atoms/section-card.astro';
|
||||||
import Typography from '@/atoms/typography.astro';
|
import Typography from '@/atoms/typography.astro';
|
||||||
|
import ProjectTimelineItem from '@/organisms/project-timeline-item.astro';
|
||||||
import type { Section } from '@/types/data';
|
import type { Section } from '@/types/data';
|
||||||
|
import type { I18n } from '@/types/i18n';
|
||||||
import type { PortfolioSection } from '@/types/portfolio-section';
|
import type { PortfolioSection } from '@/types/portfolio-section';
|
||||||
|
|
||||||
export interface Props extends PortfolioSection {}
|
export interface Props extends PortfolioSection {
|
||||||
|
i18n: I18n;
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
config: { title },
|
config: { title },
|
||||||
|
projects,
|
||||||
|
i18n,
|
||||||
} = Astro.props;
|
} = Astro.props;
|
||||||
|
|
||||||
const section: Section = 'portfolio';
|
const section: Section = 'portfolio';
|
||||||
|
|
@ -15,4 +22,12 @@ const section: Section = 'portfolio';
|
||||||
|
|
||||||
<SectionCard section={section}
|
<SectionCard section={section}
|
||||||
><Typography variant="section-title" id={`${section}-heading`}>{title}</Typography>
|
><Typography variant="section-title" id={`${section}-heading`}>{title}</Typography>
|
||||||
|
{
|
||||||
|
projects.map((project, index) => (
|
||||||
|
<>
|
||||||
|
<ProjectTimelineItem project={project} i18n={i18n} />
|
||||||
|
{index !== projects.length - 1 && <Divider />}
|
||||||
|
</>
|
||||||
|
))
|
||||||
|
}
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ const { seo, i18n, ...dataWithoutSeoAndI18n } = data;
|
||||||
<ExperienceSection i18n={data.i18n} jobs={data.experience.jobs} config={data.experience.config} />
|
<ExperienceSection i18n={data.i18n} jobs={data.experience.jobs} config={data.experience.config} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{data.portfolio && <PortfolioSection {...data.portfolio} />}
|
{data.portfolio && <PortfolioSection i18n={data.i18n} {...data.portfolio} />}
|
||||||
{data.testimonials && <TestimonialsSection {...data.testimonials} />}
|
{data.testimonials && <TestimonialsSection {...data.testimonials} />}
|
||||||
{data.favorites && <FavoritesSection {...data.favorites} />}
|
{data.favorites && <FavoritesSection {...data.favorites} />}
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -63,5 +63,5 @@ const i18nData: I18n = {
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="p-5">
|
<div class="p-5">
|
||||||
<ProjectTimelineItem value={project} i18n={i18nData} />
|
<ProjectTimelineItem project={project} i18n={i18nData} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue