'use client' /** * Shared "author byline" card used by article-shaped detail pages * (blog post, product release, onboarding guide, investor update). * * MOVED from the hub so any consuming app can embed it; hub call sites * import it directly and pass their platform-aware copy explicitly * (`fallbackBio={defaultAuthorFallbackBio()}` from the hub's app-config). * * Embed-readiness contract: * - `Link` / `Image` render through the embed-shims (plain `` / `` * in non-Next hosts; the real Next primitives once the host registers * them at app init). * - The avatar is proxied through the OPTIONAL ambient `ChatRuntime` * (`endpoints.imageProxyUrlPrefix`, same config `useProxiedImageUrl` * reads) — when no runtime is mounted the raw URL renders as-is, so the * component never throws outside a provider. Hosts can also inject * `proxyImageUrl` to bypass the runtime entirely. * - `fallbackBio` is a PLAIN prop (no app-config import): the lib has no * platform awareness; pass copy or leave it absent to render nothing. * * Render order: avatar → name + job title + date → bio (when present). * Returns null when `author` is empty (no card rendered). */ import React from 'react' import { Calendar, User } from 'lucide-react' import Image from '../../embed-shims/next-image' import Link from '../../embed-shims/next-link' import { cn } from '../../utils/cn' import { formatBioText } from '../../utils/format' import { getProxiedImageUrl } from '../../utils/image-proxy' import { useChatRuntime } from '../../contexts/chat-runtime-context' export interface ArticleAuthorBylineProps { /** Author display name. Required — block is hidden when null/empty. */ author: string | null /** Avatar URL. Falls back to a placeholder when null. */ avatar?: string | null /** Optional bio paragraph rendered below the name. */ bio?: string | null /** Optional job title rendered immediately under the name. */ jobTitle?: string | null /** Optional published date (ISO string or Date). Rendered next to the name. */ publishedAt?: string | Date | null /** Optional link target for the author name (e.g. the author page). */ href?: string | null /** * Fallback paragraph when `bio` is empty. Plain copy — the lib has no * platform/config awareness, so hosts that want a branded default * ("Contributing author on the {platform} platform") pass it explicitly * (the hub wrapper derives it from `getAppConfig()`). Absent/null ⇒ * nothing renders below the name when `bio` is empty. */ fallbackBio?: string | null /** Avatar size variant. `md` = 56px (default), `lg` = 64px. */ size?: 'md' | 'lg' /** * Host-injected avatar-URL mapper. Wins over the ambient-runtime proxy * resolution. Use when the host proxies images outside the `ChatRuntime` * config (e.g. a bespoke CDN rewrite). */ proxyImageUrl?: (url: string) => string className?: string } function formatDate(value: string | Date): string { const d = typeof value === 'string' ? new Date(value) : value if (Number.isNaN(d.getTime())) return '' // `timeZone: 'UTC'` keeps the SSR (server = UTC) and client (user's local tz) // renders identical — without it a published_at near a midnight boundary // formats to a different day on each side, triggering a React #418 hydration // text mismatch. Matches the convention in blog-metadata.tsx / investor-update. return d.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric', timeZone: 'UTC', }) } export function ArticleAuthorByline({ author, avatar, bio, jobTitle, publishedAt, href, fallbackBio, size = 'md', proxyImageUrl, className, }: ArticleAuthorBylineProps) { // Optional runtime — `useChatRuntime` returns null outside a provider, so // the byline works in bare embeds (raw avatar URL) and proxies whenever the // host mounted a runtime with `imageProxyUrlPrefix` (the hub always does). const runtime = useChatRuntime() if (!author) return null const proxiedAvatar = avatar ? proxyImageUrl ? proxyImageUrl(avatar) : (getProxiedImageUrl(avatar, { proxyPrefix: runtime?.endpoints.imageProxyUrlPrefix, skipDomains: runtime?.endpoints.imageProxySkipDomains, directHttps: true, }) ?? avatar) : '' // Class-driven sizing (md = 56px, lg = 64px). The numeric pair feeds only // the Image element's intrinsic width/height attributes (required by the // Next image shim) — layout comes from the classes. const avatarSizeClass = size === 'lg' ? 'w-16 h-16' : 'w-14 h-14' const avatarIconClass = size === 'lg' ? 'w-8 h-8' : 'w-7 h-7' const avatarDim = size === 'lg' ? 64 : 56 const formattedBio = formatBioText(bio ?? null) const dateLabel = publishedAt ? formatDate(publishedAt) : '' return (
{/* Avatar */}
{avatar ? ( {author} ) : (
)}
{/* Name + meta + bio */}

{href ? ( {author} ) : ( author )}

{dateLabel && ( {dateLabel} )}
{jobTitle && (

{jobTitle}

)} {formattedBio ? (

{formattedBio}

) : fallbackBio ? (

{fallbackBio}

) : null}
) }