import type { FunctionComponent } from 'preact';
import { Link } from '@/components/shared/link';
import { ResponsiveImage, RESPONSIVE_IMAGE_COVER_CLASS } from '@/components/responsive-image';
import { NewsThumb } from '@/components/shared/news-thumb';
import { TagList } from '@/components/taxonomy';
import { extractSlugFromImagePath } from '@/lib/utils/image-path-utils';
import type { ArticleSummary } from '@/lib/articles/get-all-articles';
import { formatPageTitle, formatDate } from '@/lib/utils/format-utils';
import { getDisplayTitle } from '@/lib/utils/content-type-utils';
export interface ArticleListItemProps {
title: string;
imageSlug: string;
date: string;
updatedAt: string;
description: string;
tags: string[];
href: string;
newsNo?: number;
locale?: string;
}
/**
* Convert an ArticleSummary to flat ArticleListItemProps
*/
export function articleSummaryToListItemProps(article: ArticleSummary): ArticleListItemProps {
const { slug, frontmatter } = article;
const imageSlug = frontmatter.resolvedHeroImgUrl
? extractSlugFromImagePath(frontmatter.resolvedHeroImgUrl) || ''
: '';
const urlPrefix = frontmatter.urlPrefix || '/notes';
const locale = urlPrefix.startsWith('/en') ? 'en' : 'ja';
// Use custom excerpt if available, otherwise use description
const description = frontmatter.customExcerpt || frontmatter.description || '';
// Use series name (productNameBread) if available, otherwise use the original title
// Apply formatPageTitle to handle [[prefix]] notation
// Apply getDisplayTitle to add content-type prefix (e.g., "ガイド:" vs "Guide:")
const displayTitle = formatPageTitle(
getDisplayTitle(
frontmatter.productNameBread
? frontmatter.title.replace(/:\s*[^:]+$/, `: ${frontmatter.productNameBread}`)
: frontmatter.title,
frontmatter.contentType,
locale,
),
);
return {
title: displayTitle,
imageSlug,
date: frontmatter.createdAt || frontmatter.date || '',
updatedAt: frontmatter.updatedAt || '',
description,
tags: frontmatter.tags || [],
href: `${urlPrefix}/${slug}`,
newsNo: frontmatter.news_no,
locale,
};
}
/**
* Article list item for listing pages
* Two-column grid layout matching zpaper's article-list-item pattern
*/
const ArticleListItemComponent: FunctionComponent<ArticleListItemProps> = ({
title,
imageSlug,
date,
updatedAt,
description,
tags,
href,
newsNo,
locale = 'ja',
}) => {
const createdDate = formatDate(date);
const updatedDate = formatDate(updatedAt);
const isEn = locale === 'en';
return (
<Link
to={href}
aria-label={isEn ? `Read article: ${title}` : `記事を読む: ${title}`}
className={'group block no-underline zd-invert-color-link border-t border-zd-white'}
>
{/* 1st row: Image + Title & Date */}
<div
className={'grid grid-cols-[80px_1fr] md:grid-cols-[120px_1fr] gap-x-hgap-sm pt-vgap-sm'}
>
{/* Thumbnail */}
<div>
{newsNo ? (
<div className="aspect-square overflow-hidden border border-zd-white">
<NewsThumb volume={newsNo} type="list" />
</div>
) : (
imageSlug && (
<div className="aspect-square overflow-hidden border border-zd-white">
<ResponsiveImage
slug={imageSlug}
alt={title}
className={RESPONSIVE_IMAGE_COVER_CLASS}
sizes="(max-width: 768px) 80px, 120px"
/>
</div>
)
)}
</div>
{/* Title & Date */}
<div>
<h2 className="font-futura text-sm md:text-base underline decoration-2">{title}</h2>
{createdDate && (
<div className="text-xs lg:text-sm font-thin leading-relaxed pt-vgap-xs">
{isEn ? `Created: ${createdDate}` : `作成: ${createdDate}`}
{updatedDate && (
<span className="text-zd-gray">
{` | ${isEn ? `Updated: ${updatedDate}` : `更新: ${updatedDate}`}`}
</span>
)}
</div>
)}
</div>
</div>
{/* 2nd row: Description + Tags */}
<div className="pt-vgap-xs pb-vgap-xs">
{description && (
<p className="text-xs md:text-sm xl:text-base font-thin line-clamp-2">{description}</p>
)}
{tags.length > 0 && (
<div className="pt-vgap-xs">
<TagList tags={tags} className="inline" />
</div>
)}
</div>
</Link>
);
};
export const ArticleListItem = ArticleListItemComponent;