diff --git a/src/src/app/about/page.tsx b/src/src/app/about/page.tsx
index 41d7c55..7a6977f 100644
--- a/src/src/app/about/page.tsx
+++ b/src/src/app/about/page.tsx
@@ -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";
@@ -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();
@@ -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 (
@@ -52,45 +47,68 @@ export default function About() {
/>
- {experiences.map((experience, index) => (
- (
+
{experience.description}
-
+
))}
-
+
{diploma.description}
-
+
+
+
+
+ {certifications.map((certification) => (
+
+ ))}
+
-
{skillCategories.map((category: SkillCategory) => (
-
- {category.description}
-
- {category.skills.map((skill) => {skill.name})}
-
-
+
+
+ {category.name}
+ {`${category.skills.length} skills`}
+
+
+ {category.description}
+
+ {category.skills.map((skill) => {skill.name})}
+
+
+
))}
diff --git a/src/src/app/articles/[slug]/page.tsx b/src/src/app/articles/[slug]/page.tsx
index 4e42493..74b4b67 100644
--- a/src/src/app/articles/[slug]/page.tsx
+++ b/src/src/app/articles/[slug]/page.tsx
@@ -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) {
diff --git a/src/src/app/articles/page.tsx b/src/src/app/articles/page.tsx
index 72fb68e..c5e3ea0 100644
--- a/src/src/app/articles/page.tsx
+++ b/src/src/app/articles/page.tsx
@@ -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";
@@ -15,7 +15,7 @@ export default function ArticlesPage() {
) : (
{articles.map((article) => (
-
+
))}
)}
diff --git a/src/src/components/cards/ArticleCard.tsx b/src/src/components/cards/ArticleCard.tsx
deleted file mode 100644
index cb8e7c3..0000000
--- a/src/src/components/cards/ArticleCard.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import Link from "next/link";
-import { Article } from "@/types/article";
-import Card, { CardBody, CardFooter, CardHeader, CardSubtitle, CardTitle } from "./Card";
-import { formatDate } from "@/core/date";
-import IconTag from "@/components/shared/IconTag";
-import { Text } from "@/components/shared/typography";
-
-interface ArticleCardProps {
- article: Article;
- showTags?: boolean;
-}
-
-export default function ArticleCard({ article, showTags = false }: ArticleCardProps) {
- return (
-
-
-
-
- {article.title}
-
-
-
- {formatDate(article.publishedAt)}
-
-
- {article.description && (
-
- {article.description}
-
- )}
-
- {showTags && article.tags.length > 0 && (
-
- {article.tags.map((tag) => (
- {tag}
- ))}
-
- )}
-
- );
-}
diff --git a/src/src/components/cards/ArticleListCard.tsx b/src/src/components/cards/ArticleListCard.tsx
new file mode 100644
index 0000000..6aba4ec
--- /dev/null
+++ b/src/src/components/cards/ArticleListCard.tsx
@@ -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
= ({ article, showTags = false }) => (
+
+
+
+
+ {article.title}
+
+
+
+ {formatDate(article.publishedAt)}
+
+
+ {article.description && (
+
+ {article.description}
+
+ )}
+
+ {showTags && article.tags.length > 0 && (
+
+ {article.tags.map((tag) => (
+ {tag}
+ ))}
+
+ )}
+
+);
+
+export default ArticleListCard;
diff --git a/src/src/components/cards/CardTitleWithTooltip.tsx b/src/src/components/cards/CardTitleWithTooltip.tsx
new file mode 100644
index 0000000..fda659d
--- /dev/null
+++ b/src/src/components/cards/CardTitleWithTooltip.tsx
@@ -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) => classes.filter(Boolean).join(" ");
+
+const CardTitleWithTooltip: React.FC = ({ children, className }) => {
+ const headingRef = useRef(null);
+ const [isOverflowing, setIsOverflowing] = useState(false);
+
+ useEffect(() => {
+ const element = headingRef.current;
+ if (element) {
+ setIsOverflowing(element.scrollWidth > element.clientWidth);
+ }
+ }, [children]);
+
+ const heading = (
+
+ {children}
+
+ );
+
+ return isOverflowing ? {heading} : heading;
+};
+
+export default CardTitleWithTooltip;
diff --git a/src/src/components/cards/InfoCard.tsx b/src/src/components/cards/InfoCard.tsx
new file mode 100644
index 0000000..19f0fad
--- /dev/null
+++ b/src/src/components/cards/InfoCard.tsx
@@ -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;
+ media?: ImageProps;
+ mediaSize?: InfoCardMediaSize;
+ mediaAlign?: "start" | "center";
+ children?: React.ReactNode;
+ footer?: React.ReactNode;
+ align?: "start" | "center";
+ showTitleTooltip?: boolean;
+ className?: string;
+};
+
+const mediaSizeClasses: Record = {
+ 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) => classes.filter(Boolean).join(" ");
+
+const InfoCard: React.FC = ({
+ title,
+ subtitle,
+ details,
+ media,
+ mediaSize = "medium",
+ mediaAlign = "start",
+ children,
+ footer,
+ align = "start",
+ showTitleTooltip = false,
+ className,
+}) => {
+ const titleNode = showTitleTooltip ? {title} : {title};
+
+ const subtitleNode =
+ typeof subtitle === "string" ? (
+ {subtitle}
+ ) : (
+ subtitle
+ );
+
+ const hasBody = Boolean(children);
+
+ return (
+
+
+ {media && (
+
+ )}
+
+
+ {titleNode}
+ {subtitleNode}
+ {details?.map((detail, idx) => (
+
+ {detail}
+
+ ))}
+
+
+ {hasBody && (
+
+ {children}
+
+ )}
+
+ {footer && (
+ {footer}
+ )}
+
+
+
+ );
+};
+
+export default InfoCard;
diff --git a/src/src/components/cards/MediaTileCard.tsx b/src/src/components/cards/MediaTileCard.tsx
new file mode 100644
index 0000000..64a7088
--- /dev/null
+++ b/src/src/components/cards/MediaTileCard.tsx
@@ -0,0 +1,40 @@
+import React from "react";
+import Card, { CardBody, CardMedia, CardTitle } from "./Card";
+import { Text } from "@/components/shared/typography";
+import { ImageProps } from "@/types";
+
+export type MediaTileSize = "small" | "medium" | "large";
+
+type MediaTileCardProps = {
+ title: string;
+ description?: string;
+ image?: ImageProps;
+ size?: MediaTileSize;
+};
+
+const sizeClasses: Record = {
+ small: "max-h-16 md:max-h-20",
+ medium: "max-h-20 md:max-h-24",
+ large: "max-h-24 md:max-h-28",
+};
+
+const MediaTileCard: React.FC = ({ title, description, image, size = "medium" }) => (
+
+
+ {image && (
+
+ )}
+
+ {title}
+ {description && {description}}
+
+
+
+);
+
+export default MediaTileCard;
diff --git a/src/src/components/cards/SmallInfoCard.tsx b/src/src/components/cards/SmallInfoCard.tsx
deleted file mode 100644
index 0261559..0000000
--- a/src/src/components/cards/SmallInfoCard.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import React from "react";
-import Card, { CardBody, CardFooter, CardMedia } from "./Card";
-import SmallInfoCardHeading from "./SmallInfoCardHeading";
-import { Secondary, Text } from "@/components/shared/typography";
-import { ImageProps } from "@/types";
-import { createId } from "@/core/string";
-
-export type SmallInfoCardImageSize = "small" | "medium" | "large";
-export type SmallInfoCardImagePosition = "center" | "start";
-
-export type SmallInfoCardProps = {
- image?: ImageProps;
- heading: string;
- subtitle?: string | React.ReactNode;
- details?: Array;
- tooltip?: string;
- children?: React.ReactNode;
- imageSize?: SmallInfoCardImageSize;
- imagePosition?: SmallInfoCardImagePosition;
- flex?: "row" | "col";
- id?: string;
-};
-
-const mediaSizeClasses: Record = {
- small: "w-10 h-10 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 mediaContainerClass = (imageSize: SmallInfoCardImageSize, imagePosition: SmallInfoCardImagePosition) => {
- const positionClass = imagePosition === "start" ? "items-start mt-1 mr-4" : "items-center";
- const sizeClass = imageSize !== "medium" && imagePosition !== "start" ? "mt-1 mr-4" : "";
- return ["flex shrink-0 justify-center", positionClass, sizeClass].filter(Boolean).join(" ");
-};
-
-const SmallInfoCard: React.FC = ({
- image,
- heading,
- subtitle,
- details,
- imageSize = "medium",
- imagePosition = "center",
- children,
- id,
- flex = "row",
-}) => {
- const cardId = id ?? createId(heading);
- const bodyFlexDirectionClass = flex === "row" ? "flex-row" : "flex-col";
-
- const containerClass = [
- "flex w-full mx-auto min-w-0 items-stretch",
- imageSize === "medium" && imagePosition !== "start" ? "gap-4" : "",
- ]
- .filter(Boolean)
- .join(" ");
-
- return (
-
-
- {image && (
-
- )}
-
- {heading}
- {subtitle && {subtitle}}
- {details?.map((detail, idx) => (
-
- {detail}
-
- ))}
- {children && (
-
- {children}
-
- )}
-
-
-
- );
-};
-
-export default SmallInfoCard;
diff --git a/src/src/components/cards/SmallInfoCardHeading.tsx b/src/src/components/cards/SmallInfoCardHeading.tsx
deleted file mode 100644
index 1d78e0a..0000000
--- a/src/src/components/cards/SmallInfoCardHeading.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-"use client";
-
-import ClientTooltip from "@/components/shared/ClientTooltip";
-import { useEffect, useRef, useState } from "react";
-import { Heading3 } from "@/components/shared/typography";
-
-type SmallInfoCardHeadingProps = { children: string };
-
-const SmallInfoCardHeading: React.FC = ({ children }) => {
- const headingReference = useRef(null);
- const [isOverflowing, setIsOverflowing] = useState(false);
-
- useEffect(() => {
- const htmlElement = headingReference.current;
- if (htmlElement) {
- setIsOverflowing(htmlElement.scrollWidth > htmlElement.clientWidth);
- }
- }, [children]);
-
- const heading = (
-
- {children}
-
- );
-
- return isOverflowing ? {heading} : heading;
-};
-
-export default SmallInfoCardHeading;
diff --git a/src/src/components/cards/SmallInfoCardsSection.tsx b/src/src/components/cards/SmallInfoCardsSection.tsx
deleted file mode 100644
index 3ad6706..0000000
--- a/src/src/components/cards/SmallInfoCardsSection.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import React from "react";
-import { CardGrid } from "./Card";
-import SmallInfoCard, { SmallInfoCardProps } from "./SmallInfoCard";
-import Section from "@/components/shared/Section";
-
-type SmallInfoCardsSectionProps = { heading: string; items: SmallInfoCardProps[] };
-
-const SmallInfoCardsGridSection: React.FC = ({ heading, items }) => (
-
-
- {items.map((item, index) => (
-
- ))}
-
-
-);
-
-export default SmallInfoCardsGridSection;
diff --git a/src/src/components/cards/ThumbnailCard.tsx b/src/src/components/cards/ThumbnailCard.tsx
deleted file mode 100644
index b229e8e..0000000
--- a/src/src/components/cards/ThumbnailCard.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { ImageProps } from "@/types";
-import Card, { CardBody, CardMedia, CardTitle } from "./Card";
-import { Text } from "@/components/shared/typography";
-
-export type ThumbnailCardImageSize = "small" | "medium" | "large" | "full";
-
-export type ThumbnailCardProps = {
- title: string;
- description?: string;
- image?: ImageProps;
- size?: ThumbnailCardImageSize;
-};
-
-const sizeClasses = { small: "w-1/4", medium: "w-1/2", large: "w-3/4", full: "w-full" };
-
-const ThumbnailCard: React.FC = ({ title, description, image, size = "medium" }) => {
- return (
-
-
- {image && (
-
- )}
-
- {title}
- {description && {description}}
-
-
-
- );
-};
-
-export default ThumbnailCard;
diff --git a/src/src/components/cards/index.ts b/src/src/components/cards/index.ts
index f310826..bc7a141 100644
--- a/src/src/components/cards/index.ts
+++ b/src/src/components/cards/index.ts
@@ -1,10 +1,8 @@
-export { default as ArticleCard } from "./ArticleCard";
-export { default as SmallInfoCard } from "./SmallInfoCard";
-export type { SmallInfoCardProps } from "./SmallInfoCard";
-export { default as SmallInfoCardHeading } from "./SmallInfoCardHeading";
-export { default as SmallInfoCardsSection } from "./SmallInfoCardsSection";
-export { default as ThumbnailCard } from "./ThumbnailCard";
-export type { ThumbnailCardImageSize, ThumbnailCardProps } from "./ThumbnailCard";
+export { default as InfoCard } from "./InfoCard";
+export { default as MediaTileCard } from "./MediaTileCard";
+export type { MediaTileSize } from "./MediaTileCard";
+export { default as ArticleListCard } from "./ArticleListCard";
+export { default as CardTitleWithTooltip } from "./CardTitleWithTooltip";
export {
default as Card,
CardBody,
diff --git a/src/src/components/home/FeaturedArticles.tsx b/src/src/components/home/FeaturedArticles.tsx
index 220beb5..63a8db8 100644
--- a/src/src/components/home/FeaturedArticles.tsx
+++ b/src/src/components/home/FeaturedArticles.tsx
@@ -1,6 +1,6 @@
import { getFeaturedArticles } from "@/core/articles";
import Section from "@/components/shared/Section";
-import { ArticleCard } from "@/components/cards";
+import { ArticleListCard } from "@/components/cards";
import EmptyState from "@/components/shared/EmptyState";
export default function FeaturedArticles() {
@@ -12,9 +12,8 @@ export default function FeaturedArticles() {
) : (
- {" "}
{featuredArticles.slice(0, 4).map((article) => (
-
+
))}
)}
diff --git a/src/src/components/shared/ThumbnailGridSection.tsx b/src/src/components/shared/ThumbnailGridSection.tsx
index e3435bf..8488f68 100644
--- a/src/src/components/shared/ThumbnailGridSection.tsx
+++ b/src/src/components/shared/ThumbnailGridSection.tsx
@@ -1,14 +1,9 @@
import Link from "next/link";
import Section from "./Section";
-import { ThumbnailCard, ThumbnailCardImageSize, CardGrid } from "@/components/cards";
+import { CardGrid, MediaTileCard } from "@/components/cards";
import NewTabLink from "./NewTabLink";
-
-interface ImageProps {
- src: string;
- alt: string;
- width?: number;
- height?: number;
-}
+import { ImageProps } from "@/types";
+import type { MediaTileSize } from "@/components/cards/MediaTileCard";
interface ThumbnailItem {
title: string;
@@ -22,7 +17,7 @@ interface ThumbnailGridSectionProps {
heading: string;
items: ThumbnailItem[];
columns?: number;
- size?: ThumbnailCardImageSize;
+ size?: MediaTileSize;
redirectPath?: string;
redirectLabel?: string;
}
@@ -39,11 +34,11 @@ const ThumbnailGridSection: React.FC = ({
{items.map((item, index) => {
const card = (
-
);