Takazudo Modular Styleguide

List/GuideArticleListItem

Default
English Locale
Flat Props
With Adapter

Component Source

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;

Story Source

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"
  />
);