diff --git a/src/components/organisms/ArrangementDialog/ArrangementDialog.stories.tsx b/src/components/organisms/ArrangementDialog/ArrangementDialog.stories.tsx new file mode 100644 index 0000000..7f982bd --- /dev/null +++ b/src/components/organisms/ArrangementDialog/ArrangementDialog.stories.tsx @@ -0,0 +1,195 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { ArrangementDialog } from './ArrangementDialog'; +import type { DialogStep, AuthValues } from './ArrangementDialog'; +import { Button } from '../../atoms/Button'; +import Box from '@mui/material/Box'; + +// ─── Mock data ────────────────────────────────────────────────────────────── + +const mockProvider = { + name: 'H.Parsons Funeral Directors', + location: 'Wentworth, NSW', + imageUrl: 'https://placehold.co/600x200/E8E0D6/8B6F47?text=H.Parsons', + rating: 4.6, + reviewCount: 7, +}; + +const mockPackage = { + name: 'Classic Funeral Package', + price: 4500, + total: 4500, + sections: [ + { + heading: 'Professional services', + items: [ + { name: 'Funeral director services' }, + { name: 'Care and preparation' }, + { name: 'Coordination with cemetery/crematorium' }, + ], + }, + { + heading: 'Venue & transport', + items: [{ name: 'Chapel service (1 hour)' }, { name: 'Hearse transport' }], + }, + { + heading: 'Included items', + items: [{ name: 'Standard coffin' }, { name: 'Memorial book' }], + }, + ], +}; + +const defaultAuthValues: AuthValues = { + subStep: 'email', + email: '', + firstName: '', + lastName: '', + phone: '', + contactPreference: 'call_anytime', + verificationCode: '', +}; + +// ─── Meta ──────────────────────────────────────────────────────────────────── + +const meta: Meta = { + title: 'Organisms/ArrangementDialog', + component: ArrangementDialog, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, +}; + +export default meta; +type Story = StoryObj; + +// ─── Interactive (default) ────────────────────────────────────────────────── + +/** Full two-step flow: preview → auth */ +export const Default: Story = { + render: () => { + const [open, setOpen] = useState(true); + const [step, setStep] = useState('preview'); + const [authValues, setAuthValues] = useState({ ...defaultAuthValues }); + + return ( + + + setOpen(false)} + step={step} + onStepChange={setStep} + provider={mockProvider} + selectedPackage={mockPackage} + authValues={authValues} + onAuthChange={setAuthValues} + onGoogleSSO={() => alert('Google SSO')} + onMicrosoftSSO={() => alert('Microsoft SSO')} + onContinue={() => alert('Auth complete')} + /> + + ); + }, +}; + +// ─── Auth step ────────────────────────────────────────────────────────────── + +/** Starting directly on the auth step */ +export const AuthStep: Story = { + render: () => { + const [open, setOpen] = useState(true); + const [step, setStep] = useState('auth'); + const [authValues, setAuthValues] = useState({ ...defaultAuthValues }); + + return ( + + + setOpen(false)} + step={step} + onStepChange={setStep} + provider={mockProvider} + selectedPackage={mockPackage} + authValues={authValues} + onAuthChange={setAuthValues} + onContinue={() => alert('Auth complete')} + /> + + ); + }, +}; + +// ─── Auth with details ────────────────────────────────────────────────────── + +/** Auth step with details sub-step open */ +export const AuthDetails: Story = { + render: () => { + const [open, setOpen] = useState(true); + const [step, setStep] = useState('auth'); + const [authValues, setAuthValues] = useState({ + ...defaultAuthValues, + subStep: 'details', + email: 'john@example.com', + }); + + return ( + + + setOpen(false)} + step={step} + onStepChange={setStep} + provider={mockProvider} + selectedPackage={mockPackage} + authValues={authValues} + onAuthChange={setAuthValues} + onContinue={() => alert('Auth complete')} + /> + + ); + }, +}; + +// ─── Pre-planning ─────────────────────────────────────────────────────────── + +/** Pre-planning variant with "Explore other options" CTA */ +export const PrePlanning: Story = { + render: () => { + const [open, setOpen] = useState(true); + const [step, setStep] = useState('preview'); + const [authValues, setAuthValues] = useState({ ...defaultAuthValues }); + + return ( + + + setOpen(false)} + step={step} + onStepChange={setStep} + provider={mockProvider} + selectedPackage={mockPackage} + authValues={authValues} + onAuthChange={setAuthValues} + onContinue={() => alert('Auth complete')} + isPrePlanning + onExplore={() => { + setOpen(false); + alert('Exploring other options'); + }} + /> + + ); + }, +}; diff --git a/src/components/organisms/ArrangementDialog/ArrangementDialog.tsx b/src/components/organisms/ArrangementDialog/ArrangementDialog.tsx new file mode 100644 index 0000000..e23cc91 --- /dev/null +++ b/src/components/organisms/ArrangementDialog/ArrangementDialog.tsx @@ -0,0 +1,541 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import Dialog from '@mui/material/Dialog'; +import DialogContent from '@mui/material/DialogContent'; +import IconButton from '@mui/material/IconButton'; +import TextField from '@mui/material/TextField'; +import MenuItem from '@mui/material/MenuItem'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import CloseIcon from '@mui/icons-material/Close'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import GoogleIcon from '@mui/icons-material/Google'; +import MicrosoftIcon from '@mui/icons-material/Window'; +import type { SxProps, Theme } from '@mui/material/styles'; +import type { PackageSection } from '../PackageDetail'; +import { ProviderCardCompact } from '../../molecules/ProviderCardCompact'; +import { Collapse } from '../../atoms/Collapse'; +import { Typography } from '../../atoms/Typography'; +import { Button } from '../../atoms/Button'; +import { Divider } from '../../atoms/Divider'; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +/** Which step of the dialog is active */ +export type DialogStep = 'preview' | 'auth'; + +/** Provider summary */ +export interface DialogProvider { + name: string; + location: string; + imageUrl?: string; + rating?: number; + reviewCount?: number; +} + +/** Package summary for preview */ +export interface DialogPackage { + name: string; + price: number; + sections: PackageSection[]; + total?: number; +} + +/** A step in the "What's next?" checklist */ +export interface NextStepItem { + number: number; + label: string; +} + +/** Auth sub-step */ +export type AuthSubStep = 'email' | 'details' | 'verify'; + +/** Contact preference */ +export type ContactPreference = 'call_anytime' | 'email_preferred' | 'email_only'; + +/** Auth form values */ +export interface AuthValues { + subStep: AuthSubStep; + email: string; + firstName: string; + lastName: string; + phone: string; + contactPreference: ContactPreference; + verificationCode: string; +} + +/** Auth field errors */ +export interface AuthErrors { + email?: string; + firstName?: string; + lastName?: string; + phone?: string; + verificationCode?: string; +} + +/** Props for the ArrangementDialog */ +export interface ArrangementDialogProps { + /** Whether the dialog is open */ + open: boolean; + /** Callback when the dialog is closed */ + onClose: () => void; + /** Current dialog step */ + step: DialogStep; + /** Callback when the step changes */ + onStepChange: (step: DialogStep) => void; + + // ─── Preview data ─── + /** Provider summary */ + provider: DialogProvider; + /** Selected package */ + selectedPackage: DialogPackage; + /** What's next checklist */ + nextSteps?: NextStepItem[]; + /** Pre-planning flow */ + isPrePlanning?: boolean; + /** Callback for "Explore other options" (pre-planning) */ + onExplore?: () => void; + + // ─── Auth data ─── + /** Auth form values */ + authValues: AuthValues; + /** Callback when auth values change */ + onAuthChange: (values: AuthValues) => void; + /** Auth field errors */ + authErrors?: AuthErrors; + /** Callback for Google SSO */ + onGoogleSSO?: () => void; + /** Callback for Microsoft SSO */ + onMicrosoftSSO?: () => void; + + // ─── Actions ─── + /** Callback when the final auth CTA is clicked */ + onContinue: () => void; + /** Loading state for CTAs */ + loading?: boolean; + + /** MUI sx prop */ + sx?: SxProps; +} + +// ─── Constants ────────────────────────────────────────────────────────────── + +const DEFAULT_NEXT_STEPS: NextStepItem[] = [ + { number: 1, label: 'Create your account to save your selections' }, + { number: 2, label: 'Choose a date and time for the service' }, + { number: 3, label: 'Select a venue' }, + { number: 4, label: 'Choose a coffin' }, + { number: 5, label: 'Review and confirm your arrangement' }, +]; + +// ─── Helpers ──────────────────────────────────────────────────────────────── + +function getAuthCTALabel(subStep: AuthSubStep): string { + switch (subStep) { + case 'email': + return 'Continue with email'; + case 'details': + return 'Continue'; + case 'verify': + return 'Verify and continue'; + } +} + +// ─── Component ─────────────────────────────────────────────────────────────── + +/** + * Two-step arrangement dialog (D-E). + * + * Consolidates PreviewStep + AuthGateStep into a single modal: + * - **Step 1 (preview):** Package summary, provider info, "What's next" checklist + * - **Step 2 (auth):** SSO buttons, email entry, details, verification + * + * The dialog is opened after a user selects a package (from PackagesStep). + * The parent controls which step is shown and manages auth form state. + * + * Pure presentation component — props in, callbacks out. + */ +export const ArrangementDialog: React.FC = ({ + open, + onClose, + step, + onStepChange, + provider, + selectedPackage, + nextSteps = DEFAULT_NEXT_STEPS, + isPrePlanning = false, + onExplore, + authValues, + onAuthChange, + authErrors, + onGoogleSSO, + onMicrosoftSSO, + onContinue, + loading = false, + sx, +}) => { + const isEmailOnly = authValues.contactPreference === 'email_only'; + + const handleAuthField = (field: keyof AuthValues, value: string) => { + onAuthChange({ ...authValues, [field]: value }); + }; + + return ( + + {/* ─── Header ─── */} + + + {step === 'auth' && ( + onStepChange('preview')} + aria-label="Back to preview" + > + + + )} + + {step === 'preview' ? 'Your selected package' : 'Save your plan'} + + + + + + + + + {/* ═══════════ Step 1: Preview ═══════════ */} + {step === 'preview' && ( + + {/* Provider */} + + + + + {/* Package summary */} + + + {selectedPackage.name} + + ${(selectedPackage.total ?? selectedPackage.price).toLocaleString('en-AU')} + + + + {selectedPackage.sections.map((section) => ( + + + {section.heading} + + {section.items.map((item) => ( + + {item.name} + + ))} + + ))} + + + + You'll be able to customise everything in the next steps. + + + {/* What's next */} + + + What happens next + + + {nextSteps.map((s) => ( + + + + {s.number} + + + + + ))} + + + + + + {/* CTAs */} + + {isPrePlanning && onExplore && ( + + )} + + + + )} + + {/* ═══════════ Step 2: Auth ═══════════ */} + {step === 'auth' && ( + { + e.preventDefault(); + if (!loading) onContinue(); + }} + > + + {isPrePlanning + ? 'Save your plan to return and update it anytime.' + : 'We need a few details so a funeral arranger can help you with the next steps.'} + + + {/* SSO buttons */} + + + + + + + + or + + + + {/* Email */} + handleAuthField('email', e.target.value)} + error={!!authErrors?.email} + helperText={authErrors?.email} + placeholder="you@example.com" + autoComplete="email" + inputMode="email" + fullWidth + required + disabled={authValues.subStep !== 'email'} + sx={{ mb: 3 }} + /> + + {/* Details (after email) */} + + + + A few details to save your plan + + + + handleAuthField('firstName', e.target.value)} + error={!!authErrors?.firstName} + helperText={authErrors?.firstName} + autoComplete="given-name" + fullWidth + required + disabled={authValues.subStep === 'verify'} + /> + handleAuthField('lastName', e.target.value)} + error={!!authErrors?.lastName} + helperText={authErrors?.lastName} + autoComplete="family-name" + fullWidth + required + disabled={authValues.subStep === 'verify'} + /> + + + handleAuthField('phone', e.target.value)} + error={!!authErrors?.phone} + helperText={authErrors?.phone} + placeholder="e.g. 0412 345 678" + autoComplete="tel" + inputMode="tel" + fullWidth + required={!isEmailOnly} + disabled={authValues.subStep === 'verify'} + /> + + handleAuthField('contactPreference', e.target.value)} + fullWidth + disabled={authValues.subStep === 'verify'} + > + Call me anytime + Email is preferred + Only contact by email + + + + + {/* Verification */} + + + + We've sent a 6-digit code to {authValues.email}. Please + enter it below. + + + handleAuthField('verificationCode', e.target.value)} + error={!!authErrors?.verificationCode} + helperText={ + authErrors?.verificationCode || 'Check your email for the 6-digit code' + } + placeholder="Enter 6-digit code" + autoComplete="one-time-code" + inputMode="numeric" + fullWidth + required + /> + + + + {/* Terms */} + + By continuing, you agree to the{' '} + + terms and conditions + + . + + + + + {/* CTA */} + + + + + )} + + + ); +}; + +ArrangementDialog.displayName = 'ArrangementDialog'; +export default ArrangementDialog; diff --git a/src/components/organisms/ArrangementDialog/index.ts b/src/components/organisms/ArrangementDialog/index.ts new file mode 100644 index 0000000..e2efd05 --- /dev/null +++ b/src/components/organisms/ArrangementDialog/index.ts @@ -0,0 +1,13 @@ +export { ArrangementDialog } from './ArrangementDialog'; +export type { + ArrangementDialogProps, + DialogStep, + DialogProvider, + DialogPackage, + NextStepItem, + AuthSubStep, + ContactPreference, + AuthValues, + AuthErrors, +} from './ArrangementDialog'; +export { default } from './ArrangementDialog';