'use client' /** * CaseStudyCard (pure presentation). Two densities — `default` (vertical * detail) and `sm` (compact horizontal for chat-inline). * * The card writes NO click logic — callers wrap with their own anchor * and pass the resolved detail URL via `href`. * * Image-fallback chain: * `study.featured_image` → `placeholderUrl` (caller passes * `useOgPlaceholderUrl(...)`) → `bg-ods-bg`. */ import React from 'react' import Image from '../../../embed-shims/next-image' import { Card } from '../../ui/card' import { cn } from '../../../utils/cn' import type { CaseStudy } from '../../../types/case-study' import { EntityPortraitCard } from './entity-portrait-card' import { useEntityCardLink } from './use-entity-card-link' import { useEntityCardPlaceholder } from './use-entity-card-placeholder' import { COMPACT_CARD_IMAGE_SLOT, COMPACT_CARD_META_ROW_BOX, COMPACT_CARD_OUTER, COMPACT_CARD_ROW_FILLER, COMPACT_CARD_SKELETON_IMAGE_SLOT, COMPACT_CARD_SKELETON_OUTER, COMPACT_CARD_SUBTITLE, COMPACT_CARD_SUMMARY, COMPACT_CARD_TEXT_COL, COMPACT_CARD_TITLE, COMPACT_CARD_TITLE_ROW, } from '../utils/compact-card-classes' import { hideOnError } from './use-cover-image-fallback' export interface CaseStudyCardProps { study: CaseStudy /** Detail URL resolved by the caller. */ href: string /** When `_blank`, opens in a new tab. Set by chat dispatch via * `computeIsNewTab`. Defaults to same-tab. */ target?: '_blank' rel?: 'noopener noreferrer' targetPlatform?: string | null /** OG placeholder URL, used when `study.featured_image` is missing. */ placeholderUrl?: string | null size?: 'default' | 'sm' | 'portrait' /** Portrait density: render the content-type chip. Mixed rails only; single-type rails pass false. Default true. */ showTypeBadge?: boolean className?: string } /** `portrait` shares the default skeleton shape — the portrait anatomy uses the * same zone boxes (media aspect → 72px title → 60px person footer). */ export function CaseStudyCardSkeleton({ size = 'default' }: { size?: 'default' | 'sm' | 'portrait' }) { if (size === 'sm') { return ( ) } return (
{/* Skeleton aspect matches the real card's image slot (OG 1200×630) */}
) } export function CaseStudyCard({ study, href, target: targetProp, rel: relProp, targetPlatform, placeholderUrl: placeholderUrlProp, size = 'default', showTypeBadge = true, className, }: CaseStudyCardProps) { const { target, rel } = useEntityCardLink({ href, targetPlatform, target: targetProp, rel: relProp, }) const placeholderUrl = useEntityCardPlaceholder({ title: study.title, placeholderUrl: placeholderUrlProp, aspect: size === 'sm' ? 'square' : 'wide', }) const coverImage = study.featured_image || placeholderUrl || null if (size === 'sm') { return ( {coverImage ? ( {`${study.msp?.name ) : null} {study.title} {[study.msp?.name, study.user?.full_name].filter(Boolean).join(' · ') || 'Case study'} {study.summary || COMPACT_CARD_ROW_FILLER} ) } if (size === 'portrait') { // Rail/strip density — shared shell. return ( ) } return ( {/* Fixed aspect ratio matches the standard OG card source aspect (1200×630 = 1.91:1), so the image fits with near-zero CSS-side cropping. Subject anchoring is then a function of the source image's center-66% safe zone (OG design convention) — visually uniform across all cards regardless of column width. Skeleton (lines 73-74) uses the same aspect for consistent layout. */}
{coverImage && ( {study.msp?.name )}

{study.title}

{study.user?.avatar_url ? ( {study.user?.full_name ) : (
{(study.user?.full_name || 'A').charAt(0).toUpperCase()}
)} {study.msp?.icon_url && (
{study.msp.name
)}

{study.user?.full_name || 'Anonymous'} {study.msp?.name && • {study.msp.name}}

{study.user?.job_title || ' '}

) }