Takazudo Modular Styleguide

List/ArticleListItem

Default
News Article
No Description
With Custom Excerpt
With Image
With Tags

Component Source

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 { 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-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 ArticleListItem = ArticleListItemComponent;

Story Source

article-list-item.stories.tsx

import { ArticleListItem } from './article-list-item';
import type { ControlsMap } from '../../sub-packages/styleguide-v2/src/data/control-types';

export const meta = {
  title: 'List/ArticleListItem',
};

export const controls: ControlsMap = {
  title: { type: 'text', default: 'Mutable Instruments Plaits 商品詳細' },
  description: {
    type: 'text',
    default:
      'Plaitsはマクロオシレーターの後継モジュールで、16種類のシンセシスモデルを搭載しています。',
  },
};

/** Default - article with image, description, and tags */
export const Default = {
  args: {
    title: 'Mutable Instruments Plaits 商品詳細',
    description:
      'Plaitsはマクロオシレーターの後継モジュールで、16種類のシンセシスモデルを搭載しています。',
  },
  render: ({ title, description }: { title: string; description: string }) => (
    <ArticleListItem
      title={title}
      imageSlug="https://takazudomodular.com/images/p/1-front/1200w.webp"
      date="2024-06-15"
      updatedAt="2024-08-20"
      description={description}
      tags={['product-intro', 'oscillator']}
      href="/products/plaits-intro/"
    />
  ),
};

/** With image - article showing a product thumbnail */
export const WithImage = () => (
  <ArticleListItem
    title="ADDAC104 VC Integrator 商品詳細"
    imageSlug="https://takazudomodular.com/images/p/addac104-0-front/1200w.webp"
    date="2024-03-10"
    updatedAt=""
    description="ADDAC104はCV信号を統合・処理するためのユーティリティモジュールです。"
    tags={['product-intro']}
    href="/products/addac104-intro/"
  />
);

/** With tags - article with multiple taxonomy tags */
export const WithTags = () => (
  <ArticleListItem
    title="モジュラーシンセ電源入門 その1: 電源の基礎知識"
    imageSlug="https://takazudomodular.com/images/p/ad110-1-front/1200w.webp"
    date="2024-06-01"
    updatedAt=""
    description="モジュラーシンセの電源について基礎から解説します。"
    tags={['guide', 'how-to-build', 'power']}
    href="/guides/power-guide-vol1/"
    locale="ja"
  />
);

/** No description - article with title and image only */
export const NoDescription = () => (
  <ArticleListItem
    title="2V2 デュアルVCA 商品詳細"
    imageSlug="https://takazudomodular.com/images/p/2v2-1-front/1200w.webp"
    date="2024-02-28"
    updatedAt=""
    description=""
    tags={[]}
    href="/products/2v2-intro/"
  />
);

/** News article - uses NewsThumb instead of product image */
export const NewsArticle = () => (
  <ArticleListItem
    title="Takazudo Modular News Vol.42"
    imageSlug=""
    date="2024-09-01"
    updatedAt=""
    description="新商品入荷情報やイベント情報をお届けします。"
    tags={[]}
    href="/notes/news-vol-42/"
    newsNo={42}
  />
);

/** Custom excerpt - using description for display */
export const WithCustomExcerpt = () => (
  <ArticleListItem
    title="ACIDS Operator 商品詳細"
    imageSlug="https://takazudomodular.com/images/p/acids-0-front/1200w.webp"
    date="2024-07-20"
    updatedAt=""
    description="ACIDSのOperatorモジュールは独創的なFMシンセシスを実現します。"
    tags={['product-intro', 'oscillator']}
    href="/products/acids-intro/"
  />
);