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
8 changes: 8 additions & 0 deletions src/api/ghost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ interface GhostResponse<T> {
posts?: T[]
pages?: T[]
tags?: T[]
authors?: T[]
meta: {
pagination: {
page: number
Expand Down Expand Up @@ -113,3 +114,10 @@ export async function getTags() {
const data = await ghostFetch<GhostResponse<GhostTag>>("tags/?limit=all")
return data.tags ?? []
}

export async function getAuthor(slug: string) {
const data = await ghostFetch<GhostResponse<GhostAuthor>>(
`authors/slug/${slug}/`
)
return data.authors?.[0] ?? null
}
41 changes: 41 additions & 0 deletions src/components/author-bio.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { render, screen } from "@testing-library/react"
import { describe, expect, it, vi } from "vitest"

import { AuthorBio } from "./author-bio"

const useAuthorMock = vi.hoisted(() => vi.fn())

vi.mock("@/lib/use-ghost", () => ({
useAuthor: useAuthorMock,
}))

describe("AuthorBio", () => {
it("renders the fallback name, bio, and placeholder when Ghost has no data", () => {
useAuthorMock.mockReturnValue({ data: undefined })

render(<AuthorBio />)

expect(screen.getByText("Will Thomson")).toBeInTheDocument()
expect(screen.getByText(/certified arborist/i)).toBeInTheDocument()
expect(screen.queryByRole("img")).not.toBeInTheDocument()
})

it("renders the Ghost profile image, name, and bio when present", () => {
useAuthorMock.mockReturnValue({
data: {
id: "1",
name: "Will Thomson",
slug: "will",
profile_image: "https://content.example.com/will.jpg",
bio: "Arborist and author.",
},
})

render(<AuthorBio />)

const img = screen.getByRole("img", { name: "Will Thomson" })
expect(img).toHaveAttribute("src", "https://content.example.com/will.jpg")
expect(screen.getByText("Arborist and author.")).toBeInTheDocument()
expect(screen.queryByText(/certified arborist/i)).not.toBeInTheDocument()
})
})
36 changes: 30 additions & 6 deletions src/components/author-bio.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,38 @@
import { useAuthor } from "@/lib/use-ghost"

// Single-author site: the Ghost staff profile is the source of truth for the
// photo and bio, editable in Ghost Admin without a deploy. The hardcoded
// strings below are fallbacks for while the Ghost profile fields are empty
// (and for when the Content API is unreachable).
const AUTHOR_SLUG = "will"

const FALLBACK_NAME = "Will Thomson"
const FALLBACK_BIO =
"Will is a certified arborist and landscape designer. He holds an MSc in " +
"Environmental Science from the American University of Beirut and an MLA " +
"from the Spitzer School of Architecture, and is a certified hater of " +
"using trees to score political points."

export function AuthorBio() {
const { data: author } = useAuthor(AUTHOR_SLUG)

return (
<div className="flex flex-col items-center gap-4 sm:flex-row sm:items-start sm:gap-8">
<div className="bg-muted h-40 w-40 shrink-0 rounded-full" />
{author?.profile_image ? (
<img
src={author.profile_image}
alt={author.name}
className="h-40 w-40 shrink-0 rounded-full object-cover"
/>
) : (
<div className="bg-muted h-40 w-40 shrink-0 rounded-full" />
)}
<div>
<h3 className="text-xl font-semibold">Will Thomson</h3>
<h3 className="text-xl font-semibold">
{author?.name ?? FALLBACK_NAME}
</h3>
<p className="text-muted-foreground mt-2 leading-relaxed">
Will is a certified arborist and landscape designer. He holds an MSc
in Environmental Science from the American University of Beirut and an
MLA from the Spitzer School of Architecture, and is a certified hater
of using trees to score political points.
{author?.bio ?? FALLBACK_BIO}
</p>
</div>
</div>
Expand Down
10 changes: 9 additions & 1 deletion src/lib/use-ghost.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query"
import { getPost, getPosts, getPage, getTags } from "@/api/ghost"
import { getAuthor, getPost, getPosts, getPage, getTags } from "@/api/ghost"

export function usePosts(options?: {
page?: number
Expand Down Expand Up @@ -36,3 +36,11 @@ export function useTags() {
queryFn: getTags,
})
}

export function useAuthor(slug: string) {
return useQuery({
queryKey: ["ghost", "author", slug],
queryFn: () => getAuthor(slug),
enabled: !!slug,
})
}
Loading