Article/PurchaseNav/IncomingItem
Both
Both Locales
Neither
Notify Only
Reservation Only
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,
}),
};
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>
);