Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 53 additions & 35 deletions src/src/app/about/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import Section from "@/components/shared/Section";
import React from "react";
import MarkdownSection from "@/components/shared/MarkdownSection";
import { SmallInfoCard, SmallInfoCardProps, SmallInfoCardsSection } from "@/components/cards";
import { Card, CardBody, CardGrid, CardHeader, CardSubtitle, CardTitle } from "@/components/cards";
import InfoCard from "@/components/cards/InfoCard";
import MediaTileCard from "@/components/cards/MediaTileCard";
import { Certification, SkillCategory } from "@/types";
import { MarkdownPreview } from "@/components/shared/preview";
import { getCertifications, getDiploma, getExperiences, getProfile, getSkillCategories } from "@/core/data";
Expand All @@ -19,14 +21,13 @@ const formatExperiencePeriod = (start: Date, end?: Date) => {
return `${startDate} - ${endDate}`;
};

const certifications: SmallInfoCardProps[] = getCertifications()
const certifications = getCertifications()
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
.map((certification: Certification) => ({
image: certification.image,
heading: certification.title,
subtitle: certification.issuer,
details: [formatMonthYear(certification.date)],
imageSize: "medium" as const,
title: certification.title,
description: certification.issuer,
date: formatMonthYear(certification.date),
}));

const experiences = getExperiences();
Expand All @@ -35,12 +36,6 @@ const diploma = getDiploma();
const profile = getProfile();

export default function About() {
const bachelor: SmallInfoCardProps = {
image: diploma.logo,
heading: diploma.name,
subtitle: diploma.University,
details: diploma.details,
};

return (
<div>
Expand All @@ -52,45 +47,68 @@ export default function About() {
/>
<Section heading="Experience">
<div className="flex flex-col gap-4">
{experiences.map((experience, index) => (
<SmallInfoCard
key={index}
image={experience.company.logo}
heading={experience.name}
{experiences.map((experience) => (
<InfoCard
key={`${experience.name}-${experience.company.name}`}
title={experience.name}
subtitle={`${experience.company.name} (${experience.type})`}
imageSize="small"
imagePosition="start"
details={[
experience.company.location,
formatExperiencePeriod(experience.start, experience.end),
]}
media={experience.company.logo}
mediaSize="small"
mediaAlign="start"
align="start"
showTitleTooltip
>
<MarkdownPreview>{experience.description}</MarkdownPreview>
</SmallInfoCard>
</InfoCard>
))}
</div>
</Section>
<Section heading="Education">
<SmallInfoCard {...bachelor} imageSize="small" imagePosition="start">
<InfoCard
title={diploma.name}
subtitle={diploma.University}
details={diploma.details}
media={diploma.logo}
mediaSize="small"
mediaAlign="start"
align="start"
showTitleTooltip
>
<MarkdownPreview>{diploma.description}</MarkdownPreview>
</SmallInfoCard>
</InfoCard>
</Section>
<Section heading="Certifications">
<CardGrid columns={2} className="mt-4">
{certifications.map((certification) => (
<MediaTileCard
key={certification.title}
title={certification.title}
description={certification.description}
image={certification.image}
size="small"
/>
))}
</CardGrid>
</Section>
<SmallInfoCardsSection heading="Certifications" items={certifications} />
<Section heading="Skills">
<div className="flex flex-col gap-4">
{skillCategories.map((category: SkillCategory) => (
<SmallInfoCard
key={category.name}
heading={category.name}
subtitle={`${category.skills.length} skills`}
flex="col"
details={[]}
>
<Text>{category.description}</Text>
<div className="flex flex-wrap gap-2 mt-6">
{category.skills.map((skill) => <IconTag key={skill.name}>{skill.name}</IconTag>)}
</div>
</SmallInfoCard>
<Card key={category.name}>
<CardHeader className="gap-1.5">
<CardTitle>{category.name}</CardTitle>
<CardSubtitle>{`${category.skills.length} skills`}</CardSubtitle>
</CardHeader>
<CardBody className="pt-1 gap-2">
<Text>{category.description}</Text>
<div className="flex flex-wrap gap-2 mt-6">
{category.skills.map((skill) => <IconTag key={skill.name}>{skill.name}</IconTag>)}
</div>
</CardBody>
</Card>
))}
</div>
</Section>
Expand Down
4 changes: 2 additions & 2 deletions src/src/app/articles/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ export function generateStaticParams() {
return slugs.map((slug) => ({ slug }));
}

export default async function ArticlePage({ params }: ArticlePageProps) {
const { slug } = await params;
export default function ArticlePage({ params }: ArticlePageProps) {
const { slug } = params;
const article = getArticleBySlug(slug);

if (!article) {
Expand Down
4 changes: 2 additions & 2 deletions src/src/app/articles/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getAllArticles } from "@/core/articles";
import { ArticleCard } from "@/components/cards";
import { ArticleListCard } from "@/components/cards";
import EmptyState from "@/components/shared/EmptyState";
import { Heading1 } from "@/components/shared/typography";

Expand All @@ -15,7 +15,7 @@ export default function ArticlesPage() {
) : (
<div className="space-y-6">
{articles.map((article) => (
<ArticleCard key={article.slug} article={article} showTags />
<ArticleListCard key={article.slug} article={article} showTags />
))}
</div>
)}
Expand Down
44 changes: 0 additions & 44 deletions src/src/components/cards/ArticleCard.tsx

This file was deleted.

44 changes: 44 additions & 0 deletions src/src/components/cards/ArticleListCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Link from "next/link";
import { formatDate } from "@/core/date";
import Card, { CardBody, CardFooter, CardHeader, CardSubtitle, CardTitle } from "./Card";
import IconTag from "@/components/shared/IconTag";
import { Text } from "@/components/shared/typography";
import { Article } from "@/types/article";

type ArticleListCardProps = {
article: Article;
showTags?: boolean;
};

const ArticleListCard: React.FC<ArticleListCardProps> = ({ article, showTags = false }) => (
<Card as="article" className="flex flex-col gap-3">
<CardHeader className="gap-1.5">
<CardTitle>
<Link
href={`/articles/${article.slug}`}
className="hover:text-primary-hover transition-colors cursor-pointer"
>
{article.title}
</Link>
</CardTitle>
<CardSubtitle as="time" dateTime={article.publishedAt} className="mt-1">
{formatDate(article.publishedAt)}
</CardSubtitle>
</CardHeader>
{article.description && (
<CardBody className="pt-1">
<Text>{article.description}</Text>
</CardBody>
)}

{showTags && article.tags.length > 0 && (
<CardFooter className="mt-2">
{article.tags.map((tag) => (
<IconTag key={tag}>{tag}</IconTag>
))}
</CardFooter>
)}
</Card>
);

export default ArticleListCard;
34 changes: 34 additions & 0 deletions src/src/components/cards/CardTitleWithTooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"use client";

import { useEffect, useRef, useState } from "react";
import ClientTooltip from "@/components/shared/ClientTooltip";
import { Heading3 } from "@/components/shared/typography";

type CardTitleWithTooltipProps = {
children: string;
className?: string;
};

const join = (...classes: Array<string | undefined | null | false>) => classes.filter(Boolean).join(" ");

const CardTitleWithTooltip: React.FC<CardTitleWithTooltipProps> = ({ children, className }) => {
const headingRef = useRef<HTMLHeadingElement>(null);
const [isOverflowing, setIsOverflowing] = useState(false);

useEffect(() => {
const element = headingRef.current;
if (element) {
setIsOverflowing(element.scrollWidth > element.clientWidth);
}
}, [children]);

const heading = (
<Heading3 ref={headingRef} className={join("lg:whitespace-nowrap lg:overflow-hidden lg:text-ellipsis", className)}>
{children}
</Heading3>
);

return isOverflowing ? <ClientTooltip content={children}>{heading}</ClientTooltip> : heading;
};

export default CardTitleWithTooltip;
93 changes: 93 additions & 0 deletions src/src/components/cards/InfoCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React from "react";
import Card, { CardBody, CardFooter, CardHeader, CardMedia, CardSubtitle, CardTitle } from "./Card";
import CardTitleWithTooltip from "./CardTitleWithTooltip";
import { Secondary } from "@/components/shared/typography";
import { ImageProps } from "@/types";

type InfoCardMediaSize = "small" | "medium" | "large";

type InfoCardProps = {
title: string;
subtitle?: React.ReactNode;
details?: Array<string | React.ReactNode>;
media?: ImageProps;
mediaSize?: InfoCardMediaSize;
mediaAlign?: "start" | "center";
children?: React.ReactNode;
footer?: React.ReactNode;
align?: "start" | "center";
showTitleTooltip?: boolean;
className?: string;
};

const mediaSizeClasses: Record<InfoCardMediaSize, string> = {
small: "w-12 h-12 md:w-16 md:h-16",
medium: "w-16 h-16 md:w-20 md:h-20",
large: "w-20 h-20 md:w-24 md:h-24",
};

const cn = (...classes: Array<string | undefined | null | false>) => classes.filter(Boolean).join(" ");

const InfoCard: React.FC<InfoCardProps> = ({
title,
subtitle,
details,
media,
mediaSize = "medium",
mediaAlign = "start",
children,
footer,
align = "start",
showTitleTooltip = false,
className,
}) => {
const titleNode = showTitleTooltip ? <CardTitleWithTooltip>{title}</CardTitleWithTooltip> : <CardTitle className="leading-tight">{title}</CardTitle>;

const subtitleNode =
typeof subtitle === "string" ? (
<CardSubtitle className="whitespace-nowrap overflow-hidden text-ellipsis">{subtitle}</CardSubtitle>
) : (
subtitle
);

const hasBody = Boolean(children);

return (
<Card className={cn("scroll-mt-24", className)}>
<div className="flex w-full items-start gap-4">
{media && (
<CardMedia
media={media}
size="medium"
align={mediaAlign}
containerClassName={cn("flex shrink-0 justify-center", mediaAlign === "start" ? "items-start mt-1" : "items-center")}
className={mediaSizeClasses[mediaSize]}
/>
)}
<div className={cn("flex flex-col flex-1 min-w-0", align === "center" ? "items-center text-center" : "items-start")}>
<CardHeader className={cn("gap-1.5 w-full", align === "center" ? "items-center text-center" : "items-start")}>
{titleNode}
{subtitleNode}
{details?.map((detail, idx) => (
<Secondary key={idx} className="whitespace-nowrap overflow-hidden text-ellipsis">

Check warning on line 72 in src/src/components/cards/InfoCard.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not use Array index in keys

See more on https://sonarcloud.io/project/issues?id=lfarci_loganfarci.com&issues=AZrQItC8VwhrarSxmI8v&open=AZrQItC8VwhrarSxmI8v&pullRequest=99
{detail}
</Secondary>
))}
</CardHeader>

{hasBody && (
<CardBody className={cn("pt-1 gap-2 w-full", align === "center" ? "items-center text-center" : "items-start")}>
{children}
</CardBody>
)}

{footer && (
<CardFooter className={cn("mt-4 w-full", align === "center" ? "justify-center" : "justify-start")}>{footer}</CardFooter>
)}
</div>
</div>
</Card>
);
};

export default InfoCard;
Loading