Takazudo Modular Styleguide

Article/PurchaseNav/IncomingItem

Both
Both Locales
Neither
Notify Only
Reservation Only

Component Source

incoming-item.tsx

import type { FunctionComponent } from 'preact';
import type { PurchaseNavProduct } from '../types';
import { NonClickableStatusItem } from '../shared/non-clickable-status-item';
import { getIncomingMessage } from '../constants';
import { NotifyMeBlock } from '@/components/notify-dialog/notify-me-block';
import { ReservationBlock } from '@/components/notify-dialog/reservation-block';
import type { Locale } from '@/lib/i18n/types';

interface IncomingItemProps {
  product: PurchaseNavProduct;
  locale?: Locale;
}

export const IncomingItem: FunctionComponent<IncomingItemProps> = ({ product, locale = 'ja' }) => {
  const { showNotifyMe, showReservation, slug, name } = product;
  const hasNotifyButtons = showNotifyMe || showReservation;
  const statusMessage = getIncomingMessage(locale, { showNotifyMe, showReservation });

  return (
    <div className="flex flex-col gap-vgap-sm">
      <NonClickableStatusItem
        product={product}
        statusLabel="Coming soon..."
        statusMessage={statusMessage}
        wrapPriceInDiv={true}
      />

      {hasNotifyButtons && (
        <div className="flex flex-col gap-vgap-sm">
          {showNotifyMe && <NotifyMeBlock productSlug={slug} productName={name} locale={locale} />}
          {showReservation && (
            <ReservationBlock productSlug={slug} productName={name} locale={locale} />
          )}
        </div>
      )}
    </div>
  );
};

products.ts

import type { PurchaseNavProduct } from '../types';

/**
 * Mock brand display names for testing
 */
export const MOCK_BRAND_NAME = 'OAM';
export const MOCK_BRAND_NAME_TAKAZUDO = 'Takazudo Modular';

/**
 * Standard blurHash for mock product images
 * Used across all mock products for consistent loading placeholders
 */
export const MOCK_BLURHASH = 'LEHV6nWB2yk8pyo0adR*.7kCMdnj';

/**
 * Mock URL constants
 */
export const MOCK_URLS = {
  mercariProduct: 'https://mercari-shops.com/shops/mock-shop/items/mock-id',
  contact: '/contact/',
} as const;

/**
 * Creates a fully-typed mock product for testing.
 *
 * Mirrors the serializable shape produced at SSR by
 * `src/lib/resolve-purchase-nav-client-props.ts` (#476): brand name
 * pre-resolved, image slug pre-extracted, optional pre-resolved metadata.
 */
export const createMockProduct = (
  overrides: Partial<PurchaseNavProduct> = {},
): PurchaseNavProduct => ({
  slug: 'mock-product',
  name: 'Mock Product',
  brandName: MOCK_BRAND_NAME,
  imageSlug: 'oam-tiny-view1',
  imageMetadata: undefined,
  ...overrides,
});

/**
 * Pre-configured mock products for PurchaseNav status testing
 */
export const mockMercariProducts = {
  available: createMockProduct({
    slug: 'mock-available',
    name: 'Available Product',
    mercariProductId: 'mock-mercari-id-available',
    detailHref: '/products/mock-available-intro/',
    price: 153800,
  }),
  sold: createMockProduct({
    slug: 'mock-sold',
    name: 'Sold Out Product',
    mercariProductId: 'mock-mercari-id-sold',
    mercariStatus: 'sold',
    detailHref: '/products/mock-sold-intro/',
    price: 89000,
  }),
  incoming: createMockProduct({
    slug: 'mock-incoming',
    name: 'Incoming Product',
    mercariProductId: 'mock-mercari-id-incoming',
    mercariStatus: 'incoming',
    detailHref: '/products/mock-incoming-intro/',
    price: 45000,
  }),
  askToBuy: createMockProduct({
    slug: 'mock-ask-to-buy',
    name: 'Ask to Buy Product',
    mercariProductId: 'mock-mercari-id-asktobuy',
    mercariStatus: 'askToBuy',
    detailHref: '/products/mock-ask-to-buy-intro/',
    price: 275000,
  }),
  discontinued: createMockProduct({
    slug: 'mock-discontinued',
    name: 'Discontinued Product',
    mercariProductId: 'mock-mercari-id-discontinued',
    mercariStatus: 'discontinued',
    detailHref: '/products/mock-discontinued-intro/',
    price: 32000,
  }),
  unavailable: createMockProduct({
    slug: 'mock-unavailable',
    name: 'Unavailable Product',
    mercariProductId: 'mock-mercari-id-unavailable',
    mercariStatus: 'unavailable',
    detailHref: '/products/mock-unavailable-intro/',
    price: 28000,
  }),
  // Flagged incoming variants used by incoming-item.stories.tsx
  incomingBoth: createMockProduct({
    slug: 'mock-incoming-both',
    name: 'Incoming Both Buttons',
    mercariProductId: 'mock-mercari-id-incoming-both',
    mercariStatus: 'incoming',
    detailHref: '/products/mock-incoming-both-intro/',
    price: 45000,
    showNotifyMe: true,
    showReservation: true,
  }),
  incomingNotify: createMockProduct({
    slug: 'mock-incoming-notify',
    name: 'Incoming Notify Only',
    mercariProductId: 'mock-mercari-id-incoming-notify',
    mercariStatus: 'incoming',
    detailHref: '/products/mock-incoming-notify-intro/',
    price: 45000,
    showNotifyMe: true,
  }),
  incomingReservation: createMockProduct({
    slug: 'mock-incoming-reservation',
    name: 'Incoming Reservation Only',
    mercariProductId: 'mock-mercari-id-incoming-reservation',
    mercariStatus: 'incoming',
    detailHref: '/products/mock-incoming-reservation-intro/',
    price: 45000,
    showReservation: true,
  }),
};

/**
 * Pre-configured mock products for RelatedProducts testing
 */
export const mockRelatedProducts = {
  product1: createMockProduct({
    slug: 'mock-product-1',
    name: 'Mock Product One',
    detailHref: '/products/mock-product-1-intro/',
  }),
  product2: createMockProduct({
    slug: 'mock-product-2',
    name: 'Mock Product Two',
    detailHref: '/products/mock-product-2-intro/',
  }),
  product3: createMockProduct({
    slug: 'mock-product-3',
    name: 'Mock Product Three',
    brandName: MOCK_BRAND_NAME_TAKAZUDO,
    detailHref: '/products/mock-product-3-intro/',
  }),
  productNoDetail: createMockProduct({
    slug: 'mock-product-no-detail',
    name: 'Mock Product Without Detail',
    detailHref: undefined,
  }),
};

Story Source

incoming-item.stories.tsx

import { IncomingItem } from './status-items/incoming-item';
import { mockMercariProducts } from './__mocks__/products';

/**
 * IncomingItem Component Stories
 *
 * Renders the right-column block for incoming (近日入荷予定) products.
 * Shows "Coming soon..." status with a locale-specific message and,
 * when the flags are set, the action buttons.
 *
 * Button relationship: NotifyMeBlock ("入荷通知を受け取る") and
 * ReservationBlock ("予約する") live as standalone components under
 * `components/notify-dialog/` (also shown individually in the Notify/*
 * styleguide stories). IncomingItem COMBINES them in-context when
 * `showNotifyMe` and/or `showReservation` are set on the product — these
 * stories are the only place in the styleguide where both buttons appear
 * together inside the PurchaseNav layout.
 *
 * Four distinct messages (confirmed in constants.ts):
 *   Both:             "近日入荷予定。入荷通知及びご予約が可能な商品です。"
 *   NotifyMe only:    "近日入荷予定。入荷通知が可能な商品です。"
 *   Reservation only: "近日入荷予定。ご予約が可能な商品です。"
 *   Neither:          "近日入荷予定。"
 *
 * Mock products come from the shared `mockMercariProducts` registry
 * (single source of truth): `incomingBoth` / `incomingNotify` /
 * `incomingReservation`, plus the base `incoming` (no flags) for Neither.
 */

export const meta = {
  title: 'Article/PurchaseNav/IncomingItem',
};

/**
 * Both notify and reservation buttons — combined message
 * "近日入荷予定。入荷通知及びご予約が可能な商品です。"
 */
export const Both = () => (
  <div className="font-futura bg-zd-dark p-vgap-sm">
    <IncomingItem product={mockMercariProducts.incomingBoth} locale="ja" />
  </div>
);

/**
 * Notify-me button only — "近日入荷予定。入荷通知が可能な商品です。"
 */
export const NotifyOnly = () => (
  <div className="font-futura bg-zd-dark p-vgap-sm">
    <IncomingItem product={mockMercariProducts.incomingNotify} locale="ja" />
  </div>
);

/**
 * Reservation button only — "近日入荷予定。ご予約が可能な商品です。"
 */
export const ReservationOnly = () => (
  <div className="font-futura bg-zd-dark p-vgap-sm">
    <IncomingItem product={mockMercariProducts.incomingReservation} locale="ja" />
  </div>
);

/**
 * Neither button — "近日入荷予定。" only (base incoming, no flags)
 */
export const Neither = () => (
  <div className="font-futura bg-zd-dark p-vgap-sm">
    <IncomingItem product={mockMercariProducts.incoming} locale="ja" />
  </div>
);

/**
 * Both locales side by side for the "Both" flag combination
 */
export const BothLocales = () => (
  <div className="font-futura bg-zd-dark flex flex-col gap-vgap-sm p-vgap-sm">
    <div>
      <div className="text-zd-gray text-xs mb-vgap-2xs">JA</div>
      <IncomingItem product={mockMercariProducts.incomingBoth} locale="ja" />
    </div>
    <div>
      <div className="text-zd-gray text-xs mb-vgap-2xs">EN</div>
      <IncomingItem product={mockMercariProducts.incomingBoth} locale="en" />
    </div>
  </div>
);