Batch 5: ArrangementDialog organism — two-step modal (D-E)
New organism consolidating PreviewStep + AuthGateStep into a single MUI Dialog with two internal steps: - Step 1 (preview): ProviderCardCompact, package summary with sections/items/total, "What happens next" checklist, Continue CTA (pre-planning adds "Explore other options") - Step 2 (auth): SSO buttons (Google/Microsoft), email entry, progressive disclosure for details (name, phone, contact pref), verification code, terms Parent controls step state + auth form values. Dialog has back arrow to return from auth → preview, close button on both steps. Stories: Default (full flow), AuthStep, AuthDetails, PrePlanning PreviewStep and AuthGateStep kept for now — to be deprecated once ArrangementDialog is wired into the flow. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<typeof ArrangementDialog> = {
|
||||||
|
title: 'Organisms/ArrangementDialog',
|
||||||
|
component: ArrangementDialog,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof ArrangementDialog>;
|
||||||
|
|
||||||
|
// ─── Interactive (default) ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Full two-step flow: preview → auth */
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [open, setOpen] = useState(true);
|
||||||
|
const [step, setStep] = useState<DialogStep>('preview');
|
||||||
|
const [authValues, setAuthValues] = useState<AuthValues>({ ...defaultAuthValues });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Button variant="contained" onClick={() => setOpen(true)}>
|
||||||
|
Open dialog
|
||||||
|
</Button>
|
||||||
|
<ArrangementDialog
|
||||||
|
open={open}
|
||||||
|
onClose={() => 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')}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Auth step ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Starting directly on the auth step */
|
||||||
|
export const AuthStep: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [open, setOpen] = useState(true);
|
||||||
|
const [step, setStep] = useState<DialogStep>('auth');
|
||||||
|
const [authValues, setAuthValues] = useState<AuthValues>({ ...defaultAuthValues });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Button variant="contained" onClick={() => setOpen(true)}>
|
||||||
|
Open dialog
|
||||||
|
</Button>
|
||||||
|
<ArrangementDialog
|
||||||
|
open={open}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
step={step}
|
||||||
|
onStepChange={setStep}
|
||||||
|
provider={mockProvider}
|
||||||
|
selectedPackage={mockPackage}
|
||||||
|
authValues={authValues}
|
||||||
|
onAuthChange={setAuthValues}
|
||||||
|
onContinue={() => alert('Auth complete')}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Auth with details ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Auth step with details sub-step open */
|
||||||
|
export const AuthDetails: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [open, setOpen] = useState(true);
|
||||||
|
const [step, setStep] = useState<DialogStep>('auth');
|
||||||
|
const [authValues, setAuthValues] = useState<AuthValues>({
|
||||||
|
...defaultAuthValues,
|
||||||
|
subStep: 'details',
|
||||||
|
email: 'john@example.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Button variant="contained" onClick={() => setOpen(true)}>
|
||||||
|
Open dialog
|
||||||
|
</Button>
|
||||||
|
<ArrangementDialog
|
||||||
|
open={open}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
step={step}
|
||||||
|
onStepChange={setStep}
|
||||||
|
provider={mockProvider}
|
||||||
|
selectedPackage={mockPackage}
|
||||||
|
authValues={authValues}
|
||||||
|
onAuthChange={setAuthValues}
|
||||||
|
onContinue={() => alert('Auth complete')}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Pre-planning ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Pre-planning variant with "Explore other options" CTA */
|
||||||
|
export const PrePlanning: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [open, setOpen] = useState(true);
|
||||||
|
const [step, setStep] = useState<DialogStep>('preview');
|
||||||
|
const [authValues, setAuthValues] = useState<AuthValues>({ ...defaultAuthValues });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Button variant="contained" onClick={() => setOpen(true)}>
|
||||||
|
Open dialog
|
||||||
|
</Button>
|
||||||
|
<ArrangementDialog
|
||||||
|
open={open}
|
||||||
|
onClose={() => 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');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
541
src/components/organisms/ArrangementDialog/ArrangementDialog.tsx
Normal file
541
src/components/organisms/ArrangementDialog/ArrangementDialog.tsx
Normal file
@@ -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<Theme>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 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<ArrangementDialogProps> = ({
|
||||||
|
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 (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
maxWidth="sm"
|
||||||
|
fullWidth
|
||||||
|
scroll="body"
|
||||||
|
aria-labelledby="arrangement-dialog-title"
|
||||||
|
sx={sx}
|
||||||
|
PaperProps={{
|
||||||
|
sx: { borderRadius: 3 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* ─── Header ─── */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
px: 3,
|
||||||
|
pt: 2.5,
|
||||||
|
pb: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
{step === 'auth' && (
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => onStepChange('preview')}
|
||||||
|
aria-label="Back to preview"
|
||||||
|
>
|
||||||
|
<ArrowBackIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
<Typography id="arrangement-dialog-title" variant="h5">
|
||||||
|
{step === 'preview' ? 'Your selected package' : 'Save your plan'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<IconButton size="small" onClick={onClose} aria-label="Close">
|
||||||
|
<CloseIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<DialogContent sx={{ px: 3, pb: 3 }}>
|
||||||
|
{/* ═══════════ Step 1: Preview ═══════════ */}
|
||||||
|
{step === 'preview' && (
|
||||||
|
<Box>
|
||||||
|
{/* Provider */}
|
||||||
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<ProviderCardCompact
|
||||||
|
name={provider.name}
|
||||||
|
location={provider.location}
|
||||||
|
imageUrl={provider.imageUrl}
|
||||||
|
rating={provider.rating}
|
||||||
|
reviewCount={provider.reviewCount}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Package summary */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'var(--fa-color-brand-50)',
|
||||||
|
borderRadius: 2,
|
||||||
|
p: 2.5,
|
||||||
|
mb: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'baseline',
|
||||||
|
mb: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6">{selectedPackage.name}</Typography>
|
||||||
|
<Typography variant="h6" color="primary">
|
||||||
|
${(selectedPackage.total ?? selectedPackage.price).toLocaleString('en-AU')}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{selectedPackage.sections.map((section) => (
|
||||||
|
<Box key={section.heading} sx={{ mb: 1.5 }}>
|
||||||
|
<Typography variant="labelSm" color="text.secondary" sx={{ mb: 0.5 }}>
|
||||||
|
{section.heading}
|
||||||
|
</Typography>
|
||||||
|
{section.items.map((item) => (
|
||||||
|
<Typography key={item.name} variant="body2" sx={{ pl: 1 }}>
|
||||||
|
{item.name}
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||||
|
You'll be able to customise everything in the next steps.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* What's next */}
|
||||||
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<Typography variant="h6" sx={{ mb: 1.5 }}>
|
||||||
|
What happens next
|
||||||
|
</Typography>
|
||||||
|
<List disablePadding>
|
||||||
|
{nextSteps.map((s) => (
|
||||||
|
<ListItem key={s.number} disablePadding sx={{ mb: 1, alignItems: 'flex-start' }}>
|
||||||
|
<ListItemIcon sx={{ minWidth: 36, mt: 0.25 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
borderRadius: '50%',
|
||||||
|
bgcolor: 'var(--fa-color-brand-100)',
|
||||||
|
color: 'var(--fa-color-brand-700)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{s.number}
|
||||||
|
</Box>
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary={s.label}
|
||||||
|
primaryTypographyProps={{ variant: 'body2', color: 'text.primary' }}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider sx={{ mb: 3 }} />
|
||||||
|
|
||||||
|
{/* CTAs */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: { xs: 'column', sm: 'row' },
|
||||||
|
gap: 2,
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isPrePlanning && onExplore && (
|
||||||
|
<Button variant="outlined" color="secondary" size="large" onClick={onExplore}>
|
||||||
|
Explore other options
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="large"
|
||||||
|
onClick={() => onStepChange('auth')}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
Continue with this package
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ═══════════ Step 2: Auth ═══════════ */}
|
||||||
|
{step === 'auth' && (
|
||||||
|
<Box
|
||||||
|
component="form"
|
||||||
|
noValidate
|
||||||
|
aria-busy={loading}
|
||||||
|
onSubmit={(e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!loading) onContinue();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body1" color="text.secondary" sx={{ mb: 4 }}>
|
||||||
|
{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.'}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* SSO buttons */}
|
||||||
|
<Box
|
||||||
|
sx={{ display: 'flex', flexDirection: 'column', gap: 1.5, mb: 3 }}
|
||||||
|
role="group"
|
||||||
|
aria-label="Sign in options"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
size="large"
|
||||||
|
fullWidth
|
||||||
|
startIcon={<GoogleIcon />}
|
||||||
|
onClick={onGoogleSSO}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Continue with Google
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
size="large"
|
||||||
|
fullWidth
|
||||||
|
startIcon={<MicrosoftIcon />}
|
||||||
|
onClick={onMicrosoftSSO}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Continue with Microsoft
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider sx={{ my: 3 }}>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
or
|
||||||
|
</Typography>
|
||||||
|
</Divider>
|
||||||
|
|
||||||
|
{/* Email */}
|
||||||
|
<TextField
|
||||||
|
label="Your email address"
|
||||||
|
type="email"
|
||||||
|
value={authValues.email}
|
||||||
|
onChange={(e) => 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) */}
|
||||||
|
<Collapse in={authValues.subStep === 'details' || authValues.subStep === 'verify'}>
|
||||||
|
<Box
|
||||||
|
sx={{ display: 'flex', flexDirection: 'column', gap: 2.5, mb: 3 }}
|
||||||
|
role="group"
|
||||||
|
aria-label="Your details"
|
||||||
|
>
|
||||||
|
<Typography variant="labelLg" component="h2" sx={{ mb: 0.5 }}>
|
||||||
|
A few details to save your plan
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: { xs: 'column', sm: 'row' }, gap: 2 }}>
|
||||||
|
<TextField
|
||||||
|
label="First name"
|
||||||
|
value={authValues.firstName}
|
||||||
|
onChange={(e) => handleAuthField('firstName', e.target.value)}
|
||||||
|
error={!!authErrors?.firstName}
|
||||||
|
helperText={authErrors?.firstName}
|
||||||
|
autoComplete="given-name"
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
disabled={authValues.subStep === 'verify'}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Last name"
|
||||||
|
value={authValues.lastName}
|
||||||
|
onChange={(e) => handleAuthField('lastName', e.target.value)}
|
||||||
|
error={!!authErrors?.lastName}
|
||||||
|
helperText={authErrors?.lastName}
|
||||||
|
autoComplete="family-name"
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
disabled={authValues.subStep === 'verify'}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label={isEmailOnly ? 'Phone (optional)' : 'Best number to reach you'}
|
||||||
|
type="tel"
|
||||||
|
value={authValues.phone}
|
||||||
|
onChange={(e) => 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'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
label="Contact preference"
|
||||||
|
value={authValues.contactPreference}
|
||||||
|
onChange={(e) => handleAuthField('contactPreference', e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
disabled={authValues.subStep === 'verify'}
|
||||||
|
>
|
||||||
|
<MenuItem value="call_anytime">Call me anytime</MenuItem>
|
||||||
|
<MenuItem value="email_preferred">Email is preferred</MenuItem>
|
||||||
|
<MenuItem value="email_only">Only contact by email</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
</Box>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
|
{/* Verification */}
|
||||||
|
<Collapse in={authValues.subStep === 'verify'}>
|
||||||
|
<Box sx={{ mb: 3 }} role="group" aria-label="Email verification">
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||||
|
We've sent a 6-digit code to <strong>{authValues.email}</strong>. Please
|
||||||
|
enter it below.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Verification code"
|
||||||
|
value={authValues.verificationCode}
|
||||||
|
onChange={(e) => 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
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
|
{/* Terms */}
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 3 }}>
|
||||||
|
By continuing, you agree to the{' '}
|
||||||
|
<Box
|
||||||
|
component="a"
|
||||||
|
href="#"
|
||||||
|
sx={{
|
||||||
|
color: 'var(--fa-color-text-brand)',
|
||||||
|
textDecoration: 'underline',
|
||||||
|
fontSize: 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
terms and conditions
|
||||||
|
</Box>
|
||||||
|
.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Divider sx={{ my: 3 }} />
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
|
<Button type="submit" variant="contained" size="large" loading={loading}>
|
||||||
|
{getAuthCTALabel(authValues.subStep)}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ArrangementDialog.displayName = 'ArrangementDialog';
|
||||||
|
export default ArrangementDialog;
|
||||||
13
src/components/organisms/ArrangementDialog/index.ts
Normal file
13
src/components/organisms/ArrangementDialog/index.ts
Normal file
@@ -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';
|
||||||
Reference in New Issue
Block a user