Create projects section (#99)

This commit is contained in:
Szymon Kin 2022-11-28 20:22:09 +01:00 committed by GitHub
parent 1c8c93e3c4
commit da43c079b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 88 additions and 63 deletions

View file

@ -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']}>
<Image <div class:list={['flex', 'gap-6']}>
class:list={['rounded-lg', 'object-cover', 'max-w-[120px]', 'm-0', 'overflow-hidden', 'sm:block', 'hidden']}
src={value.image}
aspectRatio="1/1"
alt={''}
format="webp"
/>
<div class:list={['col-start-2', 'col-span-2', 'sm:mx-6']}>
<div class:list={['flex', 'justify-between']}>
<Typography variant="item-title">{value.name}</Typography>
<div class:list={['fixed', 'top-3', 'right-3', 'md:flex', 'md:flex-wrap', 'gap-3', '[&>a]:my-2']}>
{value.socials?.map(({ icon, url }) => <IconButton icon={icon} href={url} target="_blank" size="small" />)}
</div>
</div>
<Timestamp
startDate={value.startDate}
endDate={value.endDate}
locale={i18n.locale}
translationForNow={i18n.translations.now}
/>
<Image <Image
class:list={['rounded-lg', 'object-cover', 'my-2', 'max-w-[120px]', 'sm:block', 'sm: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={['flex', 'md:gap-3', 'md:flex-row', 'flex-col', 'my-4']}> <div class:list={['flex', 'flex-col', 'gap-4', 'w-full']}>
<div class:list={['md:w-2/6']}> <div class:list={['flex', 'justify-between']}>
{ <div>
value.details <Typography variant="item-title">{name}</Typography>
.slice(0, Math.round(value.details.length / 2)) <Timestamp
.map((d) => <LabelledValue label={d.label} value={setLabelValue(d.value)} />) startDate={startDate}
} endDate={endDate}
locale={i18n.locale}
translationForNow={i18n.translations.now}
/>
</div>
<div class:list={['flex', 'gap-2']}>
{socials?.map(({ icon, url }) => <IconButton icon={icon} href={url} target="_blank" size="small" />)}
</div>
</div> </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 class:list={['flex', 'gap-3', 'flex-wrap', 'sm:flex-nowrap', 'mt-6']}>
{
value.tags.map((t) => (
<Tag name={t.icon} color={t.iconColor}>
{t.name}
</Tag>
))
}
</div> </div>
</div> </div>
</ProjectTimelineItem> <div class:list={['flex', 'gap-3', 'flex-wrap']}>
{
tags.map(({ name: tagName, icon, iconColor, url }) => {
return url ? (
<a href={url} target="_blank" rel="noopener noreferrer">
<Tag name={icon} color={iconColor}>
{tagName}
</Tag>
</a>
) : (
<Tag name={icon} color={iconColor}>
{tagName}
</Tag>
);
})
}
</div>
</div>

View file

@ -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']}>
<Typography variant="item-title" <div>
>{job.role} <span class="font-medium"> &#8212;&nbsp;{job.company}</span> <Typography variant="item-title"
</Typography> >{job.role} <span class="font-medium"> &#8212;&nbsp;{job.company}</span>
<div class:list={['md:flex', 'sm:flex-wrap', 'gap-3', '[&>a]:mb-2', 'md:[&>a]:my-2']}> </Typography>
</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>

View file

@ -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>

View file

@ -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>

View file

@ -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>