import { useState } from 'preact/hooks';
import { http, HttpResponse, delay } from 'msw';
import { ReservationDialog } from './reservation-dialog';
import { ActionButton } from '@/components/shared/action-button';
import { DialogStoryContainer } from '@/.storybook/dialog-story-container';
import type { ControlsMap } from '../../sub-packages/styleguide/src/data/control-types';
/**
* ReservationDialog Component Stories
*
* Dialog for reserving products that are coming soon.
* Features:
* - Name and email fields
* - Loading state during submission
* - Success/error feedback with reservation ID
* - MSW mocked API responses
*
* All stories use embedded portal container to avoid blocking styleguide UI.
*/
export const meta = {
title: 'Notify/ReservationDialog',
};
export const controls: ControlsMap = {
productName: { type: 'text', default: 'Incoming Product' },
productSlug: { type: 'text', default: 'mock-incoming' },
};
/**
* Interactive wrapper to demonstrate dialog open/close behavior.
* Uses embedded portal container to keep dialog within the story area.
*/
const DialogWrapper = ({
productSlug,
productName,
buttonLabel = '予約する',
}: {
productSlug: string;
productName?: string;
buttonLabel?: string;
}) => {
const [isOpen, setIsOpen] = useState(false);
return (
<DialogStoryContainer>
{(portalContainer) => (
<div className="p-hgap-sm">
<ActionButton onClick={() => setIsOpen(true)}>{buttonLabel}</ActionButton>
<ReservationDialog
isOpen={isOpen}
onClose={() => setIsOpen(false)}
productSlug={productSlug}
productName={productName}
portalContainer={portalContainer}
/>
</div>
)}
</DialogStoryContainer>
);
};
/**
* Default State
*
* Interactive dialog with MSW mocked API.
* Fill in name and email, then submit to see success state with reservation ID.
*/
export const Default = {
args: { productName: 'Incoming Product', productSlug: 'mock-incoming' },
render: ({ productName, productSlug }: { productName: string; productSlug: string }) => (
<DialogWrapper productSlug={productSlug} productName={productName || undefined} />
),
};
/**
* Without Product Name
*
* Dialog without specific product name shown.
*/
export const WithoutProductName = () => <DialogWrapper productSlug="mock-product" />;
/**
* Success State with Reservation ID
*
* Shows the success message including reservation ID.
* Submit the form to see the generated reservation ID.
*/
export const SuccessWithReservationId = () => (
<div>
<p className="text-zd-white pb-vgap-sm">
フォームを送信すると、予約番号付きの成功メッセージが表示されます。
</p>
<DialogWrapper productSlug="mock-incoming" productName="Sample Product" />
</div>
);
/**
* Error State (Mocked)
*
* Tests error handling with forced API failure.
*/
export const ErrorState = () => (
<DialogWrapper productSlug="mock-incoming" productName="Error Test Product" />
);
ErrorState.parameters = {
msw: {
handlers: [
http.post('/api/reservation', async () => {
await delay(1000);
return HttpResponse.json(
{
success: false,
message: 'サーバーエラーが発生しました。もう一度お試しください。',
},
{ status: 500 },
);
}),
],
},
};
/**
* Validation Error - Missing Name
*
* Tests validation when name field is empty.
*/
export const ValidationErrorMissingName = () => (
<DialogWrapper productSlug="mock-incoming" productName="Validation Test" />
);
ValidationErrorMissingName.parameters = {
msw: {
handlers: [
http.post('/api/reservation', async () => {
await delay(500);
return HttpResponse.json(
{
success: false,
message: 'お名前を入力してください。',
},
{ status: 400 },
);
}),
],
},
};
/**
* Loading State
*
* Extended loading time to demonstrate loading spinner.
*/
export const LoadingState = () => (
<DialogWrapper productSlug="mock-incoming" productName="Loading Test Product" />
);
LoadingState.parameters = {
msw: {
handlers: [
http.post('/api/reservation', async () => {
// Long delay to observe loading state
await delay(5000);
return HttpResponse.json({
success: true,
reservationId: 'RSV-TEST123-ABCD',
message: 'ご予約を承りました。',
});
}),
],
},
};
/**
* Always Open (embedded for visual testing)
*
* Dialog that starts in open state, rendered within the story container.
*/
export const AlwaysOpen = () => (
<DialogStoryContainer>
{(portalContainer) => (
<ReservationDialog
isOpen={true}
onClose={() => {}}
productSlug="mock-incoming"
productName="Visual Test Product"
portalContainer={portalContainer}
/>
)}
</DialogStoryContainer>
);
/**
* Long Product Name
*
* Tests dialog with a very long product name to check text wrapping.
*/
export const LongProductName = () => (
<DialogWrapper
productSlug="mock-long-name"
productName="Very Long Product Name That Should Wrap Nicely In The Dialog Header And Description Area"
/>
);