Skip to content

Commit f513146

Browse files
committed
feat: ✨ Add author and profile image to blog post cards
1 parent 7eea384 commit f513146

5 files changed

Lines changed: 175 additions & 62 deletions

File tree

src/components/blog/BlogFeatured.tsx

Lines changed: 108 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { motion } from 'framer-motion';
44
import Image from 'next/image';
55
import Link from 'next/link';
66

7+
import authorsData from '@/assets/data/authors.json';
8+
79
type FeaturedProps = {
810
slug: string;
911
title: string;
@@ -13,6 +15,7 @@ type FeaturedProps = {
1315
heroImage: string;
1416
imageAuthor?: string;
1517
category?: string;
18+
authors?: string[];
1619
};
1720

1821
const cardV = {
@@ -31,76 +34,128 @@ export default function BlogFeatured({
3134
heroImage,
3235
imageAuthor,
3336
category,
37+
authors,
3438
}: FeaturedProps) {
39+
const resolvedAuthors = (authors ?? []).flatMap((id) => {
40+
const data = authorsData[id as keyof typeof authorsData];
41+
return data ? [{ id, ...data }] : [];
42+
});
43+
3544
return (
3645
<motion.article
3746
initial="rest"
3847
whileHover="hover"
3948
animate="rest"
4049
variants={cardV}
4150
transition={{ type: `spring`, stiffness: 220, damping: 24 }}
42-
className="group mx-auto w-full max-w-5xl overflow-hidden rounded-2xl border border-slate-200 bg-white"
51+
className="group relative mx-auto w-full max-w-5xl overflow-hidden rounded-2xl border border-slate-200 bg-white"
4352
itemScope
4453
itemType="https://schema.org/BlogPosting"
4554
>
46-
<Link href={`/blog/${slug}`} className="relative block">
47-
<motion.div
48-
variants={mediaV}
49-
transition={{ duration: 0.25 }}
50-
className="relative aspect-[16/9] max-h-[420px] w-full overflow-hidden bg-slate-100 md:aspect-[5/2] md:max-h-[440px] lg:aspect-[21/9] lg:max-h-[460px]"
51-
>
52-
<Image
53-
src={heroImage}
54-
alt={imageAuthor || title}
55-
fill
56-
sizes="(min-width:1280px) 1100px, 100vw"
57-
className="object-cover"
58-
priority
59-
/>
60-
</motion.div>
61-
<div className="p-5 sm:p-6">
62-
<div className="mb-2 flex flex-wrap items-center gap-2 text-xs text-slate-600">
63-
<time dateTime={date}>{dayjs(date).format(`MMMM D, YYYY`)}</time>
64-
<span className="text-slate-300 select-none"></span>
65-
<span>{timeToRead} min read</span>
66-
{category && (
67-
<>
68-
<span className="text-slate-300 select-none"></span>
69-
<div
70-
aria-label={`View posts in ${category} category`}
71-
className="rounded-full border border-slate-300 px-2 py-0.5 text-[11px] font-medium text-slate-700 hover:border-slate-400 hover:text-slate-900"
55+
<motion.div
56+
variants={mediaV}
57+
transition={{ duration: 0.25 }}
58+
className="relative aspect-[16/9] max-h-[420px] w-full overflow-hidden bg-slate-100 md:aspect-[5/2] md:max-h-[440px] lg:aspect-[21/9] lg:max-h-[460px]"
59+
>
60+
<Image
61+
src={heroImage}
62+
alt={imageAuthor || title}
63+
fill
64+
sizes="(min-width:1280px) 1100px, 100vw"
65+
className="object-cover"
66+
priority
67+
/>
68+
</motion.div>
69+
70+
<div className="p-5 sm:p-6">
71+
<div className="mb-2 flex flex-wrap items-center gap-2 text-xs text-slate-600">
72+
{resolvedAuthors.length > 0 && (
73+
<>
74+
<div className="relative z-10 flex items-center gap-1.5">
75+
<Link
76+
href={`/authors/${resolvedAuthors[0].id}`}
77+
className="flex -space-x-1.5 transition-opacity hover:opacity-80"
78+
tabIndex={-1}
79+
aria-hidden
7280
>
73-
{category}
74-
</div>
75-
</>
76-
)}
77-
</div>
81+
{resolvedAuthors.slice(0, 3).map((author) => (
82+
<div
83+
key={author.name}
84+
className="relative h-6 w-6 overflow-hidden rounded-full ring-2 ring-white"
85+
>
86+
<Image
87+
src={author.avatar}
88+
alt={author.name}
89+
fill
90+
sizes="24px"
91+
className="object-cover"
92+
/>
93+
</div>
94+
))}
95+
</Link>
96+
<span>
97+
{resolvedAuthors.map((author, i) => (
98+
<span key={author.id}>
99+
{i > 0 && <span className="mr-0.5">,</span>}
100+
<Link
101+
href={`/authors/${author.id}`}
102+
className="hover:underline"
103+
>
104+
{author.name}
105+
</Link>
106+
</span>
107+
))}
108+
</span>
109+
</div>
110+
<span className="text-slate-300 select-none"></span>
111+
</>
112+
)}
113+
<time dateTime={date}>{dayjs(date).format(`MMMM D, YYYY`)}</time>
114+
<span className="text-slate-300 select-none"></span>
115+
<span>{timeToRead} min read</span>
116+
{category && (
117+
<>
118+
<span className="text-slate-300 select-none"></span>
119+
<div
120+
aria-label={`View posts in ${category} category`}
121+
className="rounded-full border border-slate-300 px-2 py-0.5 text-[11px] font-medium text-slate-700 hover:border-slate-400 hover:text-slate-900"
122+
>
123+
{category}
124+
</div>
125+
</>
126+
)}
127+
</div>
78128

79-
<motion.h2
80-
variants={titleV}
81-
transition={{ duration: 0.2 }}
82-
className="group-hover:text-primary text-2xl leading-tight font-bold text-balance text-slate-900 group-hover:underline sm:text-3xl md:text-[1.9rem]"
83-
>
84-
{title}
85-
</motion.h2>
129+
<motion.h2
130+
variants={titleV}
131+
transition={{ duration: 0.2 }}
132+
className="group-hover:text-primary text-2xl leading-tight font-bold text-balance text-slate-900 group-hover:underline sm:text-3xl md:text-[1.9rem]"
133+
>
134+
{title}
135+
</motion.h2>
86136

87-
{subtitle && (
88-
<p className="mt-2 max-w-3xl text-base text-slate-700 md:text-lg">
89-
{subtitle}
90-
</p>
91-
)}
137+
{subtitle && (
138+
<p className="mt-2 max-w-3xl text-base text-slate-700 md:text-lg">
139+
{subtitle}
140+
</p>
141+
)}
92142

93-
<div className="mt-4">
94-
<div className="group-hover:text-primary text-sm font-medium underline-offset-4 hover:underline">
95-
<span>Read the full story</span>
96-
<Icon
97-
icon="solar:arrow-right-broken"
98-
className="ml-1 inline-block h-4 w-4"
99-
/>
100-
</div>
143+
<div className="mt-4">
144+
<div className="group-hover:text-primary text-sm font-medium underline-offset-4 hover:underline">
145+
<span>Read the full story</span>
146+
<Icon
147+
icon="solar:arrow-right-broken"
148+
className="ml-1 inline-block h-4 w-4"
149+
/>
101150
</div>
102151
</div>
103-
</Link>
152+
</div>
153+
154+
<Link
155+
href={`/blog/${slug}`}
156+
className="absolute inset-0"
157+
aria-label={title}
158+
/>
104159
</motion.article>
105160
);
106161
}

src/components/blog/BlogListItem.tsx

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { motion } from 'framer-motion';
33
import Image from 'next/image';
44
import Link from 'next/link';
55

6+
import authorsData from '@/assets/data/authors.json';
67
import { slugifyTag } from '@/lib/utils';
78

89
type ListItemProps = {
@@ -15,6 +16,7 @@ type ListItemProps = {
1516
imageAuthor?: string;
1617
tags?: string[];
1718
category?: string;
19+
authors?: string[];
1820
};
1921

2022
const cardV = {
@@ -34,7 +36,13 @@ export default function BlogListItem({
3436
imageAuthor,
3537
tags = [],
3638
category,
39+
authors,
3740
}: ListItemProps) {
41+
const resolvedAuthors = (authors ?? []).flatMap((id) => {
42+
const data = authorsData[id as keyof typeof authorsData];
43+
return data ? [{ id, ...data }] : [];
44+
});
45+
3846
return (
3947
<motion.li
4048
initial="rest"
@@ -46,7 +54,7 @@ export default function BlogListItem({
4654
>
4755
<div className="grid grid-cols-[auto_1fr] items-start gap-4 sm:gap-5">
4856
<div
49-
aria-label={title}
57+
aria-hidden
5058
className="relative col-start-1 row-span-2 aspect-[16/10] w-32 shrink-0 overflow-hidden rounded-xl bg-slate-100 sm:w-44 lg:w-52"
5159
>
5260
<motion.div
@@ -64,12 +72,49 @@ export default function BlogListItem({
6472
</motion.div>
6573
</div>
6674

67-
<Link
68-
href={`/blog/${slug}`}
69-
className="col-start-2 row-start-1 block"
70-
aria-label={`Open post: ${title}`}
71-
>
72-
<div className="mb-1 flex flex-wrap items-center gap-2 text-[11px] text-slate-600">
75+
<div className="col-start-2 row-start-1 block">
76+
<div className="mb-1 flex flex-wrap items-center gap-1.5 text-[11px] text-slate-600">
77+
{resolvedAuthors.length > 0 && (
78+
<>
79+
<div className="relative z-10 flex items-center gap-1">
80+
<Link
81+
href={`/authors/${resolvedAuthors[0].id}`}
82+
className="flex -space-x-1 transition-opacity hover:opacity-80"
83+
tabIndex={-1}
84+
aria-hidden
85+
>
86+
{resolvedAuthors.slice(0, 3).map((author) => (
87+
<div
88+
key={author.name}
89+
className="relative h-5 w-5 overflow-hidden rounded-full ring-2 ring-white"
90+
>
91+
<Image
92+
src={author.avatar}
93+
alt={author.name}
94+
fill
95+
sizes="20px"
96+
className="object-cover"
97+
/>
98+
</div>
99+
))}
100+
</Link>
101+
<span>
102+
{resolvedAuthors.map((author, i) => (
103+
<span key={author.id}>
104+
{i > 0 && <span className="mr-0.5">,</span>}
105+
<Link
106+
href={`/authors/${author.id}`}
107+
className="hover:underline"
108+
>
109+
{author.name}
110+
</Link>
111+
</span>
112+
))}
113+
</span>
114+
</div>
115+
<span className="text-slate-300 select-none"></span>
116+
</>
117+
)}
73118
<time dateTime={date}>{dayjs(date).format(`MMM D, YYYY`)}</time>
74119
<span className="text-slate-300 select-none"></span>
75120
<span>{timeToRead} min read</span>
@@ -88,9 +133,9 @@ export default function BlogListItem({
88133
{subtitle}
89134
</p>
90135
)}
91-
</Link>
136+
</div>
92137

93-
<div className="col-start-2 row-start-2 flex flex-wrap items-center gap-2">
138+
<div className="relative z-10 col-start-2 row-start-2 flex flex-wrap items-center gap-2">
94139
{category && (
95140
<Link
96141
href={`/category/${encodeURIComponent(category)}`}
@@ -115,6 +160,12 @@ export default function BlogListItem({
115160
))}
116161
</div>
117162
</div>
163+
164+
<Link
165+
href={`/blog/${slug}`}
166+
className="absolute inset-0"
167+
aria-label={`Open post: ${title}`}
168+
/>
118169
</motion.li>
119170
);
120171
}

src/pages/blog/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type BlogList = {
2121
category: string;
2222
heroImage: string;
2323
imageAuthor: string;
24+
authors?: string[];
2425
};
2526
};
2627

@@ -131,6 +132,7 @@ export default function Blog({ blogList }: BlogProps) {
131132
heroImage={featured.frontMatter.heroImage}
132133
imageAuthor={featured.frontMatter.imageAuthor}
133134
category={featured.frontMatter.category}
135+
authors={featured.frontMatter.authors}
134136
/>
135137
</motion.div>
136138
</AnimatePresence>
@@ -178,6 +180,7 @@ export default function Blog({ blogList }: BlogProps) {
178180
imageAuthor={frontMatter.imageAuthor}
179181
tags={frontMatter.tags}
180182
category={frontMatter.category}
183+
authors={frontMatter.authors}
181184
/>
182185
</motion.div>
183186
))}

src/pages/category/[category].tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type BlogList = {
2020
category: string;
2121
heroImage: string;
2222
imageAuthor?: string;
23+
authors?: string[];
2324
};
2425
};
2526

@@ -85,6 +86,7 @@ const Blog: React.FC<BlogProps> = ({ filteredBlogList }) => {
8586
imageAuthor={frontMatter.imageAuthor}
8687
tags={frontMatter.tags}
8788
category={frontMatter.category}
89+
authors={frontMatter.authors}
8890
/>
8991
</ul>
9092
</motion.div>

src/pages/tags/[tag].tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type BlogList = {
2323
category: string;
2424
heroImage: string;
2525
imageAuthor: string;
26+
authors?: string[];
2627
};
2728
};
2829

@@ -99,6 +100,7 @@ const Blog: React.FC<BlogProps> = ({ filteredBlogList }) => {
99100
imageAuthor={frontMatter.imageAuthor}
100101
tags={frontMatter.tags}
101102
category={frontMatter.category}
103+
authors={frontMatter.authors}
102104
/>
103105
</ul>
104106
</motion.div>

0 commit comments

Comments
 (0)