List/GuideArticleListItem
Default
English Locale
Flat Props
With Adapter
guide-article-list-item.tsx
import type { FunctionComponent } from 'preact';
import { Link } from '@/components/shared/link';
import { ResponsiveImage, RESPONSIVE_IMAGE_COVER_CLASS } from '@/components/responsive-image';
import { TagList } from '@/components/taxonomy';
import { extractSlugFromImagePath } from '@/lib/utils/image-path-utils';
import type { ArticleSummary } from '@/lib/articles/get-all-articles';
import { formatDate } from '@/lib/utils/format-utils';
export interface GuideArticleListItemProps {
title: string;
imageSlug: string;
date: string;
updatedAt: string;
description: string;
tags: string[];
href: string;
locale?: string;
}
/**
* Convert an ArticleSummary to flat GuideArticleListItemProps
*/
export function articleSummaryToGuideListItemProps(
article: ArticleSummary,
): GuideArticleListItemProps {
const { slug, frontmatter } = article;
const imageSlug = frontmatter.resolvedHeroImgUrl
? extractSlugFromImagePath(frontmatter.resolvedHeroImgUrl) || ''
: '';
const urlPrefix = frontmatter.urlPrefix || '/guides';
const locale = urlPrefix.startsWith('/en') ? 'en' : 'ja';
return {
title: frontmatter.title,
imageSlug,
date: frontmatter.createdAt || frontmatter.date || '',
updatedAt: frontmatter.updatedAt || '',
description: frontmatter.description || '',
tags: frontmatter.tags || [],
href: `${urlPrefix}/${slug}/`,
locale,
};
}
/**
* Guide article list item card
* Two-column grid layout: [thumbnail | text content]
* Adapted from zpaper's article-list-item pattern for zmod
*/
const GuideArticleListItemComponent: FunctionComponent<GuideArticleListItemProps> = ({
title,
imageSlug,
date,
updatedAt,
description,
tags,
href,
locale = 'ja',
}) => {
const createdDate = formatDate(date);
const updatedDate = formatDate(updatedAt);
const isEn = locale === 'en';
return (
<Link
to={href}
aria-label={`${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>
{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-subtext">
{` | ${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" locale={locale as 'ja' | 'en'} />
</div>
)}
</div>
</Link>
);
};
export const GuideArticleListItem = GuideArticleListItemComponent;
guide-article-list-item.stories.tsx
import {
GuideArticleListItem,
articleSummaryToGuideListItemProps,
} from './guide-article-list-item';
import { createMockArticleSummary } from '@/components/__test-utils__/mock-article';
import type { ControlsMap } from '../../sub-packages/styleguide-v2/src/data/control-types';
export const meta = {
title: 'List/GuideArticleListItem',
};
export const controls: ControlsMap = {
title: { type: 'text', default: 'モジュラーシンセ電源入門 その1: 電源の基礎知識' },
description: { type: 'text', default: 'モジュラーシンセの電源について基礎から解説します。' },
};
/**
* Default - Japanese locale
*/
export const Default = {
args: {
title: 'モジュラーシンセ電源入門 その1: 電源の基礎知識',
description: 'モジュラーシンセの電源について基礎から解説します。',
},
render: ({ title, description }: { title: string; description: string }) => (
<GuideArticleListItem
title={title}
imageSlug="https://takazudomodular.com/images/p/col-power-denkyu/1200w.webp"
date="2024-06-01"
updatedAt="2024-08-20"
description={description}
tags={['guide', 'power']}
href="/guides/col005-power/"
locale="ja"
/>
),
};
/**
* With adapter function
*/
export const WithAdapter = () => (
<GuideArticleListItem
{...articleSummaryToGuideListItemProps(
createMockArticleSummary({
slug: 'col005-power',
title: 'モジュラーシンセ電源入門 その1: 電源の基礎知識',
description: 'モジュラーシンセの電源について基礎から解説します。',
createdAt: '2024-06-01',
updatedAt: '2024-08-20',
imageSlug: 'https://takazudomodular.com/images/p/col-power-denkyu/1200w.webp',
tags: ['guide', 'power'],
contentType: 'guides',
}),
)}
/>
);
/**
* English locale
*/
export const EnglishLocale = () => (
<GuideArticleListItem
{...articleSummaryToGuideListItemProps(
createMockArticleSummary({
slug: 'power-guide-vol1',
title: 'Modular Synth Power Guide Vol.1: Power Basics',
description: 'Learn the basics of modular synthesizer power supply.',
createdAt: '2024-06-01',
updatedAt: '2024-08-20',
imageSlug: 'https://takazudomodular.com/images/p/col-power-denkyu/1200w.webp',
tags: ['guide', 'power'],
contentType: 'guides',
urlPrefix: '/en/guides',
}),
)}
/>
);
/**
* Flat props directly - no adapter
*/
export const FlatProps = () => (
<GuideArticleListItem
title="Direct Flat Props Example"
imageSlug="https://takazudomodular.com/images/p/col-power-denkyu/1200w.webp"
date="2024-06-01"
updatedAt="2024-08-20"
description="This story uses flat props directly without the adapter function."
tags={['guide']}
href="/guides/example/"
locale="ja"
/>
);