From 976cf681fc1fb821de0d24a0685c86c34bc30f58 Mon Sep 17 00:00:00 2001 From: Logan Farci Date: Sat, 29 Nov 2025 16:01:14 +0100 Subject: [PATCH] Remove specific card and refine cards component --- src/src/app/about/page.tsx | 88 +++++++++++------- src/src/app/articles/[slug]/page.tsx | 4 +- src/src/app/articles/page.tsx | 4 +- src/src/components/cards/ArticleCard.tsx | 44 --------- src/src/components/cards/ArticleListCard.tsx | 44 +++++++++ .../components/cards/CardTitleWithTooltip.tsx | 34 +++++++ src/src/components/cards/InfoCard.tsx | 93 +++++++++++++++++++ src/src/components/cards/MediaTileCard.tsx | 40 ++++++++ src/src/components/cards/SmallInfoCard.tsx | 90 ------------------ .../components/cards/SmallInfoCardHeading.tsx | 29 ------ .../cards/SmallInfoCardsSection.tsx | 18 ---- src/src/components/cards/ThumbnailCard.tsx | 37 -------- src/src/components/cards/index.ts | 12 +-- src/src/components/home/FeaturedArticles.tsx | 5 +- .../shared/ThumbnailGridSection.tsx | 19 ++-- 15 files changed, 282 insertions(+), 279 deletions(-) delete mode 100644 src/src/components/cards/ArticleCard.tsx create mode 100644 src/src/components/cards/ArticleListCard.tsx create mode 100644 src/src/components/cards/CardTitleWithTooltip.tsx create mode 100644 src/src/components/cards/InfoCard.tsx create mode 100644 src/src/components/cards/MediaTileCard.tsx delete mode 100644 src/src/components/cards/SmallInfoCard.tsx delete mode 100644 src/src/components/cards/SmallInfoCardHeading.tsx delete mode 100644 src/src/components/cards/SmallInfoCardsSection.tsx delete mode 100644 src/src/components/cards/ThumbnailCard.tsx 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 = ( - );