@fullstackdatasolutions/articles 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +1 -0
- package/dist/index.cjs +41 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +41 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/ArticleDetailHero.tsx +44 -19
- package/src/Breadcrumb.tsx +4 -1
- package/src/__tests__/ArticleDetailHero.test.tsx +57 -0
package/package.json
CHANGED
|
@@ -22,6 +22,16 @@ function toSlug(cat: string): string {
|
|
|
22
22
|
.replaceAll(/[^a-z0-9-]/g, '')
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function resolveAvatarUrl(author: AuthorProfile, config?: ArticlesConfig): string | null {
|
|
26
|
+
if (!author.avatar) return null
|
|
27
|
+
if (author.avatar.startsWith('http://') || author.avatar.startsWith('https://')) {
|
|
28
|
+
return author.avatar
|
|
29
|
+
}
|
|
30
|
+
if (!config?.siteUrl) return null
|
|
31
|
+
const siteUrl = config.siteUrl.replace(/\/$/, '')
|
|
32
|
+
return `${siteUrl}/articles/authors/${author.slug}/${author.avatar.replace(/^\/+/, '')}`
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
export function ArticleDetailHero({
|
|
26
36
|
article,
|
|
27
37
|
categoryBasePath = '/articles/category',
|
|
@@ -128,25 +138,40 @@ export function ArticleDetailHero({
|
|
|
128
138
|
<Clock className="h-4 w-4" />
|
|
129
139
|
<span>{article.readTime}</span>
|
|
130
140
|
</div>
|
|
131
|
-
{showConfiguredAuthors &&
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
{
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
141
|
+
{showConfiguredAuthors &&
|
|
142
|
+
(() => {
|
|
143
|
+
const singleAvatarUrl =
|
|
144
|
+
authors.length === 1 ? resolveAvatarUrl(authors[0], config) : null
|
|
145
|
+
return (
|
|
146
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
|
147
|
+
{singleAvatarUrl ? (
|
|
148
|
+
<Image
|
|
149
|
+
src={singleAvatarUrl}
|
|
150
|
+
alt=""
|
|
151
|
+
width={20}
|
|
152
|
+
height={20}
|
|
153
|
+
className="h-5 w-5 rounded-full object-cover"
|
|
154
|
+
/>
|
|
155
|
+
) : (
|
|
156
|
+
<User className="h-4 w-4" />
|
|
157
|
+
)}
|
|
158
|
+
<span>
|
|
159
|
+
By{' '}
|
|
160
|
+
{authors.map((author, index) => (
|
|
161
|
+
<Fragment key={author.slug}>
|
|
162
|
+
{index > 0 && ', '}
|
|
163
|
+
<Link
|
|
164
|
+
href={`/articles/authors/${author.slug}`}
|
|
165
|
+
className="font-medium text-white underline-offset-4 hover:underline"
|
|
166
|
+
>
|
|
167
|
+
{author.name}
|
|
168
|
+
</Link>
|
|
169
|
+
</Fragment>
|
|
170
|
+
))}
|
|
171
|
+
</span>
|
|
172
|
+
</div>
|
|
173
|
+
)
|
|
174
|
+
})()}
|
|
150
175
|
{showLegacyAuthor && (
|
|
151
176
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
|
152
177
|
<User className="h-4 w-4" />
|
package/src/Breadcrumb.tsx
CHANGED
|
@@ -4,6 +4,8 @@ import type { BreadcrumbItem } from './articleTypes'
|
|
|
4
4
|
type BreadcrumbProps = Readonly<{
|
|
5
5
|
items: BreadcrumbItem[]
|
|
6
6
|
className?: string
|
|
7
|
+
/** Max-width utility class applied to the breadcrumb list. Default: 'max-w-7xl'. */
|
|
8
|
+
containerClassName?: string
|
|
7
9
|
showSchema?: boolean
|
|
8
10
|
separator?: string
|
|
9
11
|
}>
|
|
@@ -29,6 +31,7 @@ function getDisplayItems(items: readonly BreadcrumbItem[]): BreadcrumbItem[] {
|
|
|
29
31
|
export function Breadcrumb({
|
|
30
32
|
items,
|
|
31
33
|
className = '',
|
|
34
|
+
containerClassName = 'max-w-7xl',
|
|
32
35
|
showSchema = true,
|
|
33
36
|
separator = '>',
|
|
34
37
|
}: BreadcrumbProps) {
|
|
@@ -42,7 +45,7 @@ export function Breadcrumb({
|
|
|
42
45
|
aria-label="Breadcrumb"
|
|
43
46
|
className={`border-b border-border bg-background/80 px-4 py-3 text-sm text-muted-foreground ${className}`}
|
|
44
47
|
>
|
|
45
|
-
<ol className=
|
|
48
|
+
<ol className={`mx-auto flex items-center gap-2 overflow-hidden ${containerClassName}`}>
|
|
46
49
|
{displayItems.map((item, index) => {
|
|
47
50
|
const isLast = index === displayItems.length - 1
|
|
48
51
|
const key = `${item.name}-${index}`
|
|
@@ -193,4 +193,61 @@ describe('ArticleDetailHero', () => {
|
|
|
193
193
|
).not.toBeInTheDocument()
|
|
194
194
|
})
|
|
195
195
|
})
|
|
196
|
+
|
|
197
|
+
describe('byline author avatar', () => {
|
|
198
|
+
const authorWithAvatar: AuthorProfile = { ...configuredAuthor, avatar: 'andrew-blase.webp' }
|
|
199
|
+
|
|
200
|
+
it('renders the resolved avatar image for a single configured author with an avatar and config.siteUrl', () => {
|
|
201
|
+
const { container } = render(
|
|
202
|
+
<ArticleDetailHero
|
|
203
|
+
article={baseArticle}
|
|
204
|
+
authors={[authorWithAvatar]}
|
|
205
|
+
config={{ siteUrl: 'https://example.com', siteName: 'Test Site' }}
|
|
206
|
+
/>
|
|
207
|
+
)
|
|
208
|
+
const img = container.querySelector('img.rounded-full')
|
|
209
|
+
expect(img).toHaveAttribute(
|
|
210
|
+
'src',
|
|
211
|
+
'https://example.com/articles/authors/andrew-blase/andrew-blase.webp'
|
|
212
|
+
)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('renders an already-absolute avatar URL unchanged, without needing config', () => {
|
|
216
|
+
const authorWithAbsoluteAvatar: AuthorProfile = {
|
|
217
|
+
...configuredAuthor,
|
|
218
|
+
avatar: 'https://cdn.example.com/andrew-blase.webp',
|
|
219
|
+
}
|
|
220
|
+
const { container } = render(
|
|
221
|
+
<ArticleDetailHero article={baseArticle} authors={[authorWithAbsoluteAvatar]} />
|
|
222
|
+
)
|
|
223
|
+
const img = container.querySelector('img.rounded-full')
|
|
224
|
+
expect(img).toHaveAttribute('src', 'https://cdn.example.com/andrew-blase.webp')
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('falls back to the icon when the author has no avatar', () => {
|
|
228
|
+
const { container } = render(
|
|
229
|
+
<ArticleDetailHero article={baseArticle} authors={[configuredAuthor]} />
|
|
230
|
+
)
|
|
231
|
+
expect(container.querySelector('img.rounded-full')).not.toBeInTheDocument()
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('falls back to the icon when a relative avatar is set but config.siteUrl is missing', () => {
|
|
235
|
+
const { container } = render(
|
|
236
|
+
<ArticleDetailHero article={baseArticle} authors={[authorWithAvatar]} />
|
|
237
|
+
)
|
|
238
|
+
expect(container.querySelector('img.rounded-full')).not.toBeInTheDocument()
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('falls back to the icon when multiple authors are configured, even if one has an avatar', () => {
|
|
242
|
+
const secondAuthor: AuthorProfile = { name: 'Jamie Rivera', slug: 'jamie-rivera', bio: '' }
|
|
243
|
+
const { container } = render(
|
|
244
|
+
<ArticleDetailHero
|
|
245
|
+
article={baseArticle}
|
|
246
|
+
authors={[authorWithAvatar, secondAuthor]}
|
|
247
|
+
config={{ siteUrl: 'https://example.com', siteName: 'Test Site' }}
|
|
248
|
+
/>
|
|
249
|
+
)
|
|
250
|
+
expect(container.querySelector('img.rounded-full')).not.toBeInTheDocument()
|
|
251
|
+
})
|
|
252
|
+
})
|
|
196
253
|
})
|