frontend phase 0: app shells, design system & data/contract patterns
Turn the starter into the Balinyaar foundation for the three actor
experiences and lock in the patterns later phases copy.
- Cleanup: remove toastDemo namespace, placeholder home page, and the two
dead icons; fix BottomBar to use usePathname (locale-aware active tab).
- Three actor shells under (private-routes), no layout above [locale]:
customer (customer) group with the 5-tab bottom nav; nurse (/nurse) and
admin (/admin) on the shared sidebar engine. Role model via constants/roles
+ useActorRole (defaults to customer until roles land in f1-b2).
- services/{domain} reference (patients) with a mock behind a config seam,
hierarchical query keys, deliberate staleTime, and mutation invalidation;
shared ApiEnvelope/Paginated wire types + unwrap() in lib/api/types.
- Money (integer-safe IRR/Toman) + Shamsi-date utils; toEnglishDigits helper.
- Shared composites, each tested: OtpInput, PhoneNumberField, StepperHeader,
StatusChip, PlaceholderScreen.
- i18n: seed nav/common/shell/patients in both locales; document namespace
conventions. Update client/CLAUDE.md Project Structure + fix ColorSchemeScript
doc drift. Add phase report, STATUS, and REQ-001 (envelope/casing/pagination).
Gate: npm run check + test:ci green (72 tests); build green with NEXT_PUBLIC_API_URL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { useState } from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { ThemeProvider } from '../../theme';
|
||||
import OtpInput from './OtpInput';
|
||||
|
||||
function Harness({ length = 4, onComplete }: { length?: number; onComplete?: (v: string) => void }) {
|
||||
const [value, setValue] = useState('');
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<OtpInput length={length} value={value} onChange={setValue} onComplete={onComplete} aria-label="code" />
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
describe('<OtpInput/> component', () => {
|
||||
it('renders one box per length', () => {
|
||||
render(<Harness length={5} />);
|
||||
expect(screen.getAllByRole('textbox')).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('accepts a digit into the first box', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Harness length={4} />);
|
||||
const boxes = screen.getAllByRole('textbox') as HTMLInputElement[];
|
||||
await user.type(boxes[0], '7');
|
||||
expect(boxes[0].value).toBe('7');
|
||||
});
|
||||
|
||||
it('normalizes Persian digits', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Harness length={4} />);
|
||||
const boxes = screen.getAllByRole('textbox') as HTMLInputElement[];
|
||||
await user.type(boxes[0], '۹');
|
||||
expect(boxes[0].value).toBe('9');
|
||||
});
|
||||
|
||||
it('calls onComplete with the full code once every box is filled', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onComplete = jest.fn();
|
||||
render(<Harness length={4} onComplete={onComplete} />);
|
||||
const boxes = screen.getAllByRole('textbox') as HTMLInputElement[];
|
||||
await user.type(boxes[0], '1');
|
||||
await user.type(boxes[1], '2');
|
||||
await user.type(boxes[2], '3');
|
||||
await user.type(boxes[3], '4');
|
||||
expect(onComplete).toHaveBeenCalledWith('1234');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,135 @@
|
||||
'use client';
|
||||
import { ChangeEvent, ClipboardEvent, FunctionComponent, KeyboardEvent, useEffect, useMemo, useRef } from 'react';
|
||||
import { Stack, TextField } from '@mui/material';
|
||||
import { digitsOnly } from '@/utils';
|
||||
|
||||
export interface OtpInputProps {
|
||||
/** Number of digit boxes. */
|
||||
length?: number;
|
||||
/** Controlled value — the digits entered so far. */
|
||||
value: string;
|
||||
/** Emits the full concatenated value on every change. */
|
||||
onChange: (value: string) => void;
|
||||
/** Fired once the value reaches `length` digits. */
|
||||
onComplete?: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
autoFocus?: boolean;
|
||||
error?: boolean;
|
||||
/** Accessible label for the group. */
|
||||
'aria-label'?: string;
|
||||
}
|
||||
|
||||
const DEFAULT_LENGTH = 5;
|
||||
const BOX_SIZE = 48;
|
||||
|
||||
/**
|
||||
* One-time-code input: `length` single-digit boxes with auto-advance, backspace-to-previous,
|
||||
* and paste distribution. Digits are normalized (Persian/Arabic → ASCII) and the group is
|
||||
* forced LTR so codes read left-to-right even inside the RTL (`fa`) layout.
|
||||
* @component OtpInput
|
||||
*/
|
||||
const OtpInput: FunctionComponent<OtpInputProps> = ({
|
||||
length = DEFAULT_LENGTH,
|
||||
value,
|
||||
onChange,
|
||||
onComplete,
|
||||
disabled,
|
||||
autoFocus,
|
||||
error,
|
||||
'aria-label': ariaLabel,
|
||||
}) => {
|
||||
const inputRefs = useRef<Array<HTMLInputElement | null>>([]);
|
||||
|
||||
const chars = useMemo(
|
||||
() => Array.from({ length }, (_, index) => value[index] ?? ''),
|
||||
[value, length]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoFocus) inputRefs.current[0]?.focus();
|
||||
}, [autoFocus]);
|
||||
|
||||
const focusBox = (index: number) => {
|
||||
const target = inputRefs.current[index];
|
||||
if (target) {
|
||||
target.focus();
|
||||
target.select();
|
||||
}
|
||||
};
|
||||
|
||||
const emit = (next: string[]) => {
|
||||
const joined = next.join('');
|
||||
onChange(joined);
|
||||
if (next.every((char) => char !== '')) onComplete?.(joined);
|
||||
};
|
||||
|
||||
const handleChange = (index: number, event: ChangeEvent<HTMLInputElement>) => {
|
||||
const incoming = digitsOnly(event.target.value);
|
||||
const next = [...chars];
|
||||
|
||||
if (!incoming) {
|
||||
next[index] = '';
|
||||
emit(next);
|
||||
return;
|
||||
}
|
||||
|
||||
if (incoming.length === 1) {
|
||||
next[index] = incoming;
|
||||
focusBox(index + 1);
|
||||
} else {
|
||||
// Paste / multi-char: fill sequentially from the current box.
|
||||
for (let offset = 0; offset < incoming.length && index + offset < length; offset += 1) {
|
||||
next[index + offset] = incoming[offset];
|
||||
}
|
||||
focusBox(Math.min(index + incoming.length, length - 1));
|
||||
}
|
||||
emit(next);
|
||||
};
|
||||
|
||||
const handleKeyDown = (index: number, event: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Backspace' && !chars[index]) {
|
||||
focusBox(index - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePaste = (index: number, event: ClipboardEvent<HTMLInputElement>) => {
|
||||
event.preventDefault();
|
||||
const pasted = digitsOnly(event.clipboardData.getData('text'));
|
||||
if (!pasted) return;
|
||||
const next = [...chars];
|
||||
for (let offset = 0; offset < pasted.length && index + offset < length; offset += 1) {
|
||||
next[index + offset] = pasted[offset];
|
||||
}
|
||||
focusBox(Math.min(index + pasted.length, length - 1));
|
||||
emit(next);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack direction="row" spacing={1} dir="ltr" role="group" aria-label={ariaLabel} sx={{ justifyContent: 'center' }}>
|
||||
{chars.map((char, index) => (
|
||||
<TextField
|
||||
key={index}
|
||||
value={char}
|
||||
disabled={disabled}
|
||||
error={error}
|
||||
onChange={(event) => handleChange(index, event as ChangeEvent<HTMLInputElement>)}
|
||||
onKeyDown={(event) => handleKeyDown(index, event as KeyboardEvent<HTMLInputElement>)}
|
||||
onPaste={(event) => handlePaste(index, event as ClipboardEvent<HTMLInputElement>)}
|
||||
inputRef={(el: HTMLInputElement | null) => {
|
||||
inputRefs.current[index] = el;
|
||||
}}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
inputMode: 'numeric',
|
||||
maxLength: 1,
|
||||
'aria-label': `${ariaLabel ?? 'digit'} ${index + 1}`,
|
||||
style: { textAlign: 'center', fontSize: '1.25rem', width: BOX_SIZE, padding: 8 },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default OtpInput;
|
||||
@@ -0,0 +1,4 @@
|
||||
import OtpInput from './OtpInput';
|
||||
|
||||
export type { OtpInputProps } from './OtpInput';
|
||||
export { OtpInput as default, OtpInput };
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useState } from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { ThemeProvider } from '../../theme';
|
||||
import PhoneNumberField, { isIranianMobile } from './PhoneNumberField';
|
||||
|
||||
function Harness({ initial = '' }: { initial?: string }) {
|
||||
const [value, setValue] = useState(initial);
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<PhoneNumberField value={value} onChange={setValue} label="Phone" />
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
describe('<PhoneNumberField/> component', () => {
|
||||
it('normalizes Persian digits to ASCII', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Harness />);
|
||||
const input = screen.getByRole('textbox') as HTMLInputElement;
|
||||
await user.type(input, '۰۹۱۲');
|
||||
expect(input.value).toBe('0912');
|
||||
});
|
||||
|
||||
it('strips non-digit characters', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Harness />);
|
||||
const input = screen.getByRole('textbox') as HTMLInputElement;
|
||||
await user.type(input, 'a0b9c1');
|
||||
expect(input.value).toBe('091');
|
||||
});
|
||||
|
||||
it('caps the value at 11 digits', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Harness />);
|
||||
const input = screen.getByRole('textbox') as HTMLInputElement;
|
||||
await user.type(input, '0912345678999');
|
||||
expect(input.value).toBe('09123456789');
|
||||
});
|
||||
|
||||
it('validates Iranian mobile numbers', () => {
|
||||
expect(isIranianMobile('09123456789')).toBe(true);
|
||||
expect(isIranianMobile('0912345678')).toBe(false);
|
||||
expect(isIranianMobile('19123456789')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
'use client';
|
||||
import { ChangeEvent, FunctionComponent } from 'react';
|
||||
import TextField, { TextFieldProps } from '@mui/material/TextField';
|
||||
import { digitsOnly } from '@/utils';
|
||||
|
||||
/** Iranian mobile numbers are 11 digits, e.g. 09123456789. */
|
||||
export const IRAN_MOBILE_LENGTH = 11;
|
||||
|
||||
/** True when `value` is a well-formed Iranian mobile number (11 digits starting 09). */
|
||||
export function isIranianMobile(value: string): boolean {
|
||||
return /^09\d{9}$/.test(value);
|
||||
}
|
||||
|
||||
export interface PhoneNumberFieldProps extends Omit<TextFieldProps, 'onChange' | 'value' | 'type'> {
|
||||
/** Controlled value — normalized ASCII digits, no formatting. */
|
||||
value: string;
|
||||
/** Emits the normalized digit string (Persian/Arabic digits converted, capped at 11). */
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iranian mobile-number field. Normalizes Persian/Arabic digits to ASCII, strips
|
||||
* non-digits, and caps length. Forces LTR digit entry so it stays correct inside the
|
||||
* RTL (`fa`) layout. Text (label/placeholder/helperText) is passed in by the caller.
|
||||
* @component PhoneNumberField
|
||||
*/
|
||||
const PhoneNumberField: FunctionComponent<PhoneNumberFieldProps> = ({ value, onChange, slotProps, ...rest }) => {
|
||||
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(digitsOnly(event.target.value).slice(0, IRAN_MOBILE_LENGTH));
|
||||
};
|
||||
|
||||
return (
|
||||
<TextField
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
dir: 'ltr',
|
||||
inputMode: 'numeric',
|
||||
maxLength: IRAN_MOBILE_LENGTH,
|
||||
style: { textAlign: 'start' },
|
||||
},
|
||||
...slotProps,
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PhoneNumberField;
|
||||
@@ -0,0 +1,5 @@
|
||||
import PhoneNumberField from './PhoneNumberField';
|
||||
|
||||
export type { PhoneNumberFieldProps } from './PhoneNumberField';
|
||||
export { IRAN_MOBILE_LENGTH, isIranianMobile } from './PhoneNumberField';
|
||||
export { PhoneNumberField as default, PhoneNumberField };
|
||||
@@ -0,0 +1,32 @@
|
||||
import { FunctionComponent } from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ThemeProvider } from '../../theme';
|
||||
import PlaceholderScreen, { PlaceholderScreenProps } from './PlaceholderScreen';
|
||||
|
||||
const ComponentToTest: FunctionComponent<PlaceholderScreenProps> = (props) => (
|
||||
<ThemeProvider>
|
||||
<PlaceholderScreen {...props} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
describe('<PlaceholderScreen/> component', () => {
|
||||
it('renders the title as a heading', () => {
|
||||
render(<ComponentToTest title="Bookings" />);
|
||||
expect(screen.getByRole('heading', { name: 'Bookings' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the description when provided', () => {
|
||||
render(<ComponentToTest title="Bookings" description="Coming soon in a later phase." />);
|
||||
expect(screen.getByText('Coming soon in a later phase.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('omits the description when not provided', () => {
|
||||
render(<ComponentToTest title="Wallet" />);
|
||||
expect(screen.queryByText(/phase/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the named icon', () => {
|
||||
const { container } = render(<ComponentToTest title="Patients" icon="patients" />);
|
||||
expect(container.querySelector('[data-icon="patients"]')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { FunctionComponent } from 'react';
|
||||
import { Stack, Typography } from '@mui/material';
|
||||
import AppIcon from '../common/AppIcon';
|
||||
|
||||
export interface PlaceholderScreenProps {
|
||||
/** Screen title (already translated by the caller). */
|
||||
title: string;
|
||||
/** Optional supporting line (already translated). */
|
||||
description?: string;
|
||||
/** Registered AppIcon name to show above the title. */
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty-state scaffold for screens whose real content lands in a later phase.
|
||||
* Presentational only — the caller passes already-translated copy so the component
|
||||
* stays i18n-agnostic and reusable across all three actor shells.
|
||||
* @component PlaceholderScreen
|
||||
*/
|
||||
const PlaceholderScreen: FunctionComponent<PlaceholderScreenProps> = ({ title, description, icon }) => (
|
||||
<Stack
|
||||
sx={{ alignItems: 'center', justifyContent: 'center', textAlign: 'center', gap: 1.5, py: 8, px: 2 }}
|
||||
>
|
||||
{icon && <AppIcon icon={icon} size={48} color="var(--bal-secondary)" />}
|
||||
<Typography variant="h5" component="h1">
|
||||
{title}
|
||||
</Typography>
|
||||
{description && (
|
||||
<Typography variant="body1" sx={{ color: 'text.secondary', maxWidth: 420 }}>
|
||||
{description}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
export default PlaceholderScreen;
|
||||
@@ -0,0 +1,4 @@
|
||||
import PlaceholderScreen from './PlaceholderScreen';
|
||||
|
||||
export type { PlaceholderScreenProps } from './PlaceholderScreen';
|
||||
export { PlaceholderScreen as default, PlaceholderScreen };
|
||||
@@ -0,0 +1,27 @@
|
||||
import { FunctionComponent } from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ThemeProvider } from '../../theme';
|
||||
import StatusChip, { StatusChipProps } from './StatusChip';
|
||||
|
||||
const ComponentToTest: FunctionComponent<StatusChipProps> = (props) => (
|
||||
<ThemeProvider>
|
||||
<StatusChip {...props} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
describe('<StatusChip/> component', () => {
|
||||
it('renders the label', () => {
|
||||
render(<ComponentToTest status="verified" label="Verified" />);
|
||||
expect(screen.getByText('Verified')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('exposes the status via a data attribute', () => {
|
||||
const { container } = render(<ComponentToTest status="pending" label="Pending" />);
|
||||
expect(container.querySelector('[data-status="pending"]')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the status icon for rejected', () => {
|
||||
const { container } = render(<ComponentToTest status="rejected" label="Rejected" />);
|
||||
expect(container.querySelector('[data-icon="rejected"]')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import { FunctionComponent } from 'react';
|
||||
import Chip, { ChipProps } from '@mui/material/Chip';
|
||||
import AppIcon from '../common/AppIcon';
|
||||
|
||||
export type StatusKind = 'verified' | 'active' | 'pending' | 'rejected' | 'info' | 'neutral';
|
||||
|
||||
interface StatusStyle {
|
||||
bg: string;
|
||||
fg: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
// Colors come from the semantic --bal-* tokens (both schemes defined in tokens.css),
|
||||
// so the chip switches with the color scheme automatically. Never hard-code a hex here.
|
||||
const STATUS_STYLE: Record<StatusKind, StatusStyle> = {
|
||||
verified: { bg: 'var(--bal-success)', fg: 'var(--bal-success-contrast)', icon: 'verified' },
|
||||
active: { bg: 'var(--bal-success)', fg: 'var(--bal-success-contrast)', icon: 'verified' },
|
||||
pending: { bg: 'var(--bal-warning)', fg: 'var(--bal-warning-contrast)', icon: 'pending' },
|
||||
rejected: { bg: 'var(--bal-error)', fg: 'var(--bal-error-contrast)', icon: 'rejected' },
|
||||
info: { bg: 'var(--bal-info)', fg: 'var(--bal-info-contrast)', icon: 'info' },
|
||||
neutral: { bg: 'var(--bal-divider)', fg: 'var(--bal-text-secondary)', icon: 'info' },
|
||||
};
|
||||
|
||||
export interface StatusChipProps extends Omit<ChipProps, 'color' | 'icon' | 'label'> {
|
||||
/** Semantic status that drives the color and icon. */
|
||||
status: StatusKind;
|
||||
/** Display text — already translated by the caller (labels are i18n keys off the code). */
|
||||
label: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Brand-harmonized status chip (verified / pending / rejected / …). Composed from MUI
|
||||
* Chip + the AppIcon registry; colors resolve from the --bal-* semantic tokens.
|
||||
* @component StatusChip
|
||||
*/
|
||||
const StatusChip: FunctionComponent<StatusChipProps> = ({ status, label, size = 'small', sx, ...rest }) => {
|
||||
const style = STATUS_STYLE[status];
|
||||
return (
|
||||
<Chip
|
||||
data-status={status}
|
||||
size={size}
|
||||
label={label}
|
||||
icon={<AppIcon icon={style.icon} size={16} color={style.fg} />}
|
||||
sx={{ backgroundColor: style.bg, color: style.fg, fontWeight: 600, ...sx }}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusChip;
|
||||
@@ -0,0 +1,4 @@
|
||||
import StatusChip from './StatusChip';
|
||||
|
||||
export type { StatusChipProps, StatusKind } from './StatusChip';
|
||||
export { StatusChip as default, StatusChip };
|
||||
@@ -0,0 +1,25 @@
|
||||
import { FunctionComponent } from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ThemeProvider } from '../../theme';
|
||||
import StepperHeader, { StepperHeaderProps } from './StepperHeader';
|
||||
|
||||
const STEPS = ['Phone', 'Verify', 'Profile'];
|
||||
|
||||
const ComponentToTest: FunctionComponent<Partial<StepperHeaderProps>> = ({ steps = STEPS, activeStep = 0 }) => (
|
||||
<ThemeProvider>
|
||||
<StepperHeader steps={steps} activeStep={activeStep} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
describe('<StepperHeader/> component', () => {
|
||||
it('renders every step label', () => {
|
||||
render(<ComponentToTest />);
|
||||
STEPS.forEach((label) => expect(screen.getByText(label)).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('marks the active step', () => {
|
||||
const { container } = render(<ComponentToTest activeStep={1} />);
|
||||
// MUI flags the active step's icon with the Mui-active class.
|
||||
expect(container.querySelector('.Mui-active')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { FunctionComponent } from 'react';
|
||||
import Stepper from '@mui/material/Stepper';
|
||||
import Step from '@mui/material/Step';
|
||||
import StepLabel from '@mui/material/StepLabel';
|
||||
|
||||
export interface StepperHeaderProps {
|
||||
/** Ordered step labels — already translated by the caller. */
|
||||
steps: string[];
|
||||
/** Zero-based index of the active step. */
|
||||
activeStep: number;
|
||||
/** Optional alternative-label layout (labels under the dots). Defaults to true. */
|
||||
alternativeLabel?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Progress header for multi-step flows (onboarding + verification). Wraps MUI Stepper;
|
||||
* direction is handled by the RTL-aware theme, so it flips correctly at `/fa`.
|
||||
* @component StepperHeader
|
||||
*/
|
||||
const StepperHeader: FunctionComponent<StepperHeaderProps> = ({ steps, activeStep, alternativeLabel = true }) => (
|
||||
<Stepper activeStep={activeStep} alternativeLabel={alternativeLabel} sx={{ py: 2 }}>
|
||||
{steps.map((label) => (
|
||||
<Step key={label}>
|
||||
<StepLabel>{label}</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
);
|
||||
|
||||
export default StepperHeader;
|
||||
@@ -0,0 +1,4 @@
|
||||
import StepperHeader from './StepperHeader';
|
||||
|
||||
export type { StepperHeaderProps } from './StepperHeader';
|
||||
export { StepperHeader as default, StepperHeader };
|
||||
@@ -19,6 +19,19 @@ import PersonIcon from '@mui/icons-material/Person';
|
||||
import ExitToAppIcon from '@mui/icons-material/ExitToApp';
|
||||
import NotificationsIcon from '@mui/icons-material/NotificationsOutlined';
|
||||
import DangerousIcon from '@mui/icons-material/Dangerous';
|
||||
import EventNoteIcon from '@mui/icons-material/EventNote';
|
||||
import GroupsIcon from '@mui/icons-material/Groups';
|
||||
import PeopleAltIcon from '@mui/icons-material/PeopleAlt';
|
||||
import WalletIcon from '@mui/icons-material/AccountBalanceWallet';
|
||||
import PersonOutlineIcon from '@mui/icons-material/AccountCircleOutlined';
|
||||
import DashboardIcon from '@mui/icons-material/Dashboard';
|
||||
import VerifiedUserIcon from '@mui/icons-material/VerifiedUser';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import MedicalServicesIcon from '@mui/icons-material/MedicalServices';
|
||||
import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
|
||||
/**
|
||||
* List of all available Icon names
|
||||
@@ -53,4 +66,17 @@ export const ICONS /* Note: Setting type disables property autocomplete :( was -
|
||||
logout: ExitToAppIcon,
|
||||
notifications: NotificationsIcon,
|
||||
error: DangerousIcon,
|
||||
bookings: EventNoteIcon,
|
||||
patients: GroupsIcon,
|
||||
users: PeopleAltIcon,
|
||||
wallet: WalletIcon,
|
||||
profile: PersonOutlineIcon,
|
||||
dashboard: DashboardIcon,
|
||||
verification: VerifiedUserIcon,
|
||||
verified: CheckCircleIcon,
|
||||
pending: HourglassEmptyIcon,
|
||||
rejected: CancelIcon,
|
||||
visits: MedicalServicesIcon,
|
||||
admin: AdminPanelSettingsIcon,
|
||||
add: AddIcon,
|
||||
};
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { FunctionComponent } from 'react';
|
||||
import { IconProps } from '../utils';
|
||||
|
||||
const CurrencyIcon: FunctionComponent<IconProps> = (props) => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 36 36" {...props}>
|
||||
<path
|
||||
fill="#D99E82"
|
||||
d="M35.222 33.598c-.647-2.101-1.705-6.059-2.325-7.566-.501-1.216-.969-2.438-1.544-3.014-.575-.575-1.553-.53-2.143.058 0 0-2.469 1.675-3.354 2.783-1.108.882-2.785 3.357-2.785 3.357-.59.59-.635 1.567-.06 2.143.576.575 1.798 1.043 3.015 1.544 1.506.62 5.465 1.676 7.566 2.325.359.11 1.74-1.271 1.63-1.63z"
|
||||
/>
|
||||
<path
|
||||
fill="#EA596E"
|
||||
d="M13.643 5.308c1.151 1.151 1.151 3.016 0 4.167l-4.167 4.168c-1.151 1.15-3.018 1.15-4.167 0L1.141 9.475c-1.15-1.151-1.15-3.016 0-4.167l4.167-4.167c1.15-1.151 3.016-1.151 4.167 0l4.168 4.167z"
|
||||
/>
|
||||
<path fill="#FFCC4D" d="M31.353 23.018l-4.17 4.17-4.163 4.165L7.392 15.726l8.335-8.334 15.626 15.626z" />
|
||||
<path
|
||||
fill="#292F33"
|
||||
d="M32.078 34.763s2.709 1.489 3.441.757c.732-.732-.765-3.435-.765-3.435s-2.566.048-2.676 2.678z"
|
||||
/>
|
||||
<path fill="#CCD6DD" d="M2.183 10.517l8.335-8.335 5.208 5.209-8.334 8.335z" />
|
||||
<path
|
||||
fill="#99AAB5"
|
||||
d="M3.225 11.558l8.334-8.334 1.042 1.042L4.267 12.6zm2.083 2.086l8.335-8.335 1.042 1.042-8.335 8.334z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default CurrencyIcon;
|
||||
@@ -1,125 +0,0 @@
|
||||
import { FunctionComponent } from 'react';
|
||||
import { IconProps } from '../utils';
|
||||
|
||||
const YellowPlaneIcon: FunctionComponent<IconProps> = (props) => {
|
||||
const styleOpacityAndEnableBackground = {
|
||||
opacity: 0.2,
|
||||
// enableBackground: 'new'
|
||||
};
|
||||
|
||||
return (
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" xmlSpace="preserve" {...props}>
|
||||
<path
|
||||
style={{ fill: '#FFCE00' }}
|
||||
d="M142.21,493.991c12.991,12.991,34.057,12.991,47.05,0c12.991-12.991,12.991-34.057,0-47.048
|
||||
L65.057,322.742c-12.993-12.992-34.059-12.992-47.05,0s-12.989,34.055,0,47.048L142.21,493.991z"
|
||||
/>
|
||||
<circle style={{ fill: '#7D868C' }} cx="386.857" cy="125.141" r="35.932" />
|
||||
<path
|
||||
style={styleOpacityAndEnableBackground}
|
||||
d="M391.846,120.154c-9.556-9.556-18.727-17.187-27.59-22.948
|
||||
c-0.969,0.785-1.908,1.627-2.807,2.527c-14.033,14.034-14.033,36.786,0,50.816c14.031,14.034,36.786,14.034,50.816,0
|
||||
c0.908-0.907,1.754-1.853,2.546-2.829C409.07,138.902,401.453,129.759,391.846,120.154z"
|
||||
/>
|
||||
<path
|
||||
style={styleOpacityAndEnableBackground}
|
||||
d="M75.949,333.636c-15.914,18.935-28.002,33.038-32.3,37.337
|
||||
c-4.221,4.221-7.777,8.86-10.672,13.785l94.264,94.266c4.925-2.894,9.565-6.449,13.789-10.672
|
||||
c4.298-4.301,18.399-16.384,37.334-32.303L75.949,333.636z"
|
||||
/>
|
||||
<path
|
||||
style={{ fill: '#333E48' }}
|
||||
d="M384.037,127.964c-30.663-30.663-63.439-47.604-94.099-16.941
|
||||
C254.182,146.782,79.164,363.201,57.52,384.842c-19.227,19.231-19.23,50.409,0,69.636c19.23,19.233,50.409,19.228,69.636,0
|
||||
c21.644-21.641,238.063-196.657,273.82-232.416C431.639,191.4,414.698,158.626,384.037,127.964z"
|
||||
/>
|
||||
<circle style={{ fill: '#7D868C' }} cx="218.905" cy="293.104" r="35.93" />
|
||||
<path
|
||||
style={styleOpacityAndEnableBackground}
|
||||
d="M278.708,123.019c-25.32,28.083-74.009,86.548-119.486,141.296
|
||||
l88.463,88.463c54.748-45.477,113.215-94.162,141.296-119.487L278.708,123.019z"
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
style={{ fill: '#FFCE00' }}
|
||||
d="M384.46,458.665c27.283,27.281,71.513,27.279,98.795-0.003c27.28-27.279,27.283-71.511,0-98.793
|
||||
L152.13,28.746c-27.283-27.283-71.513-27.283-98.793,0c-27.283,27.281-27.285,71.514-0.002,98.793L384.46,458.665z"
|
||||
/>
|
||||
<path
|
||||
style={{ fill: '#FFCE00' }}
|
||||
d="M84.341,435.945c-2.121,0-4.241-0.809-5.857-2.426c-3.236-3.235-3.236-8.48-0.002-11.716
|
||||
l50.812-50.814c3.236-3.236,8.483-3.235,11.716-0.001c3.236,3.235,3.236,8.48,0,11.714l-50.812,50.814
|
||||
C88.58,435.136,86.459,435.945,84.341,435.945z"
|
||||
/>
|
||||
</g>
|
||||
<rect
|
||||
x="174.379"
|
||||
y="88.222"
|
||||
transform="matrix(-0.7071 -0.7071 0.7071 -0.7071 200.0443 399.0237)"
|
||||
style={styleOpacityAndEnableBackground}
|
||||
width="16.568"
|
||||
height="139.719"
|
||||
/>
|
||||
<rect
|
||||
x="117.601"
|
||||
y="31.432"
|
||||
transform="matrix(-0.7071 -0.7071 0.7071 -0.7071 143.2742 261.929)"
|
||||
style={styleOpacityAndEnableBackground}
|
||||
width="16.568"
|
||||
height="139.719"
|
||||
/>
|
||||
<rect
|
||||
x="345.637"
|
||||
y="259.464"
|
||||
transform="matrix(-0.7071 -0.7071 0.7071 -0.7071 371.313 812.4501)"
|
||||
style={styleOpacityAndEnableBackground}
|
||||
width="16.568"
|
||||
height="139.719"
|
||||
/>
|
||||
<rect
|
||||
x="402.427"
|
||||
y="316.259"
|
||||
transform="matrix(-0.7071 -0.7071 0.7071 -0.7071 428.1006 949.5629)"
|
||||
style={styleOpacityAndEnableBackground}
|
||||
width="16.568"
|
||||
height="139.719"
|
||||
/>
|
||||
<path
|
||||
style={{ fill: '#1E252B' }}
|
||||
d="M489.114,354.011L383.944,248.841c10.747-9.425,18.276-16.308,22.89-20.921
|
||||
c16.43-16.43,22.038-34.95,16.673-55.044c-1.363-5.108-3.454-10.308-6.267-15.628c0.296-0.281,0.596-0.553,0.885-0.842
|
||||
c2.167-2.167,4.076-4.523,5.724-7.024l41.209,41.209c1.618,1.617,3.739,2.426,5.858,2.426c2.12,0,4.24-0.808,5.858-2.426
|
||||
c3.235-3.236,3.235-8.48,0-11.716l-46.323-46.323c0.406-2.427,0.626-4.902,0.626-7.411c0-11.811-4.599-22.914-12.95-31.265
|
||||
c-8.351-8.352-19.454-12.953-31.265-12.953c-2.511,0-4.987,0.22-7.415,0.627L333.126,35.23c-3.235-3.235-8.48-3.236-11.716-0.001
|
||||
c-3.236,3.235-3.236,8.48-0.001,11.714l41.209,41.21c-2.503,1.647-4.858,3.555-7.025,5.722c-0.288,0.288-0.562,0.59-0.843,0.886
|
||||
c-5.319-2.812-10.518-4.902-15.627-6.267c-20.099-5.369-38.615,0.243-55.044,16.673c-4.61,4.611-11.492,12.142-20.921,22.893
|
||||
L157.989,22.89C143.229,8.129,123.605,0,102.733,0S62.237,8.129,47.48,22.888c-14.76,14.76-22.89,34.383-22.89,55.254
|
||||
c-0.001,20.873,8.127,40.497,22.887,55.254l114.564,114.564c-6.337,7.624-12.648,15.223-18.86,22.703
|
||||
c-19.346,23.293-38.189,45.983-53.839,64.649l-18.428-18.429c-7.848-7.848-18.284-12.17-29.383-12.17s-21.534,4.322-29.382,12.172
|
||||
c-16.198,16.199-16.198,42.559,0,58.761l25.683,25.684c-6.704,20.045-2.103,43.073,13.83,59.004
|
||||
c10.865,10.867,25.311,16.85,40.677,16.85c6.339,0,12.515-1.034,18.354-2.994l25.658,25.658c8.102,8.101,18.74,12.15,29.381,12.15
|
||||
c10.64,0,21.284-4.051,29.384-12.15c16.201-16.201,16.201-42.562-0.001-58.763l-18.43-18.43
|
||||
c18.659-15.643,41.335-34.476,64.615-53.811c7.49-6.221,15.101-12.541,22.736-18.887l114.566,114.564
|
||||
c14.757,14.756,34.379,22.885,55.251,22.887c0.002,0,0.002,0,0.004,0c20.87,0,40.495-8.129,55.254-22.89
|
||||
c14.758-14.759,22.887-34.381,22.888-55.253C512.002,388.394,503.872,368.77,489.114,354.011z M386.86,97.491
|
||||
c7.385,0,14.328,2.876,19.55,8.099c5.222,5.222,8.097,12.165,8.097,19.55c0,6.56-2.273,12.766-6.438,17.731
|
||||
c-4.96-6.686-10.995-13.587-18.174-20.765c-7.179-7.179-14.08-13.215-20.767-18.176C374.093,99.765,380.301,97.491,386.86,97.491z
|
||||
M295.795,116.881c12.28-12.282,24.689-16.218,39.053-12.38c12.86,3.434,27.034,13.026,43.329,29.323
|
||||
c16.296,16.295,25.886,30.468,29.32,43.329c3.836,14.362-0.098,26.772-12.382,39.054c-4.413,4.414-12.113,11.434-22.915,20.895
|
||||
l-97.303-97.303C284.365,128.993,291.385,121.29,295.795,116.881z M23.866,363.932c-9.74-9.742-9.74-25.593,0-35.333
|
||||
c4.717-4.718,10.992-7.317,17.666-7.317c6.675,0,12.949,2.599,17.668,7.317l19.438,19.441C65.414,363.702,55.648,375,51.662,378.986
|
||||
c-2.168,2.168-4.12,4.47-5.868,6.876L23.866,363.932z M183.401,452.801c9.742,9.741,9.742,25.59,0.001,35.331
|
||||
c-9.742,9.741-25.594,9.742-35.336,0l-21.927-21.926c2.417-1.763,4.717-3.717,6.873-5.872c3.985-3.985,15.285-13.751,30.947-26.976
|
||||
L183.401,452.801z M230.718,356.101c-53.485,44.418-99.676,82.779-109.419,92.521c-7.735,7.735-18.02,11.995-28.96,11.996
|
||||
c-10.939,0-21.225-4.26-28.961-11.997c-15.968-15.966-15.968-41.949,0-57.92c9.744-9.743,48.118-55.949,92.55-109.452
|
||||
c5.891-7.094,11.87-14.293,17.879-21.523l78.468,78.468C245.033,344.21,237.822,350.2,230.718,356.101z M477.397,452.804
|
||||
c-11.631,11.632-27.093,18.038-43.539,18.038h-0.003c-16.447-0.001-31.909-6.406-43.538-18.034L59.192,121.681
|
||||
c-11.631-11.628-18.036-27.09-18.034-43.538c0-16.447,6.406-31.91,18.037-43.541c11.631-11.629,27.093-18.034,43.539-18.034
|
||||
c16.447,0,31.91,6.405,43.54,18.036l331.124,331.123c11.63,11.629,18.036,27.093,18.036,43.54
|
||||
C495.434,425.713,489.029,441.175,477.397,452.804z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default YellowPlaneIcon;
|
||||
@@ -1,5 +1,15 @@
|
||||
export * from './common';
|
||||
|
||||
import UserInfo from './UserInfo';
|
||||
import PlaceholderScreen from './PlaceholderScreen';
|
||||
import OtpInput from './OtpInput';
|
||||
import PhoneNumberField from './PhoneNumberField';
|
||||
import StepperHeader from './StepperHeader';
|
||||
import StatusChip from './StatusChip';
|
||||
|
||||
export { UserInfo };
|
||||
export { UserInfo, PlaceholderScreen, OtpInput, PhoneNumberField, StepperHeader, StatusChip };
|
||||
export type { PlaceholderScreenProps } from './PlaceholderScreen';
|
||||
export type { OtpInputProps } from './OtpInput';
|
||||
export type { PhoneNumberFieldProps } from './PhoneNumberField';
|
||||
export type { StepperHeaderProps } from './StepperHeader';
|
||||
export type { StatusChipProps, StatusKind } from './StatusChip';
|
||||
|
||||
Reference in New Issue
Block a user