frontend phase 5 & backend phase 12
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { ThemeProvider } from '../../theme';
|
||||
|
||||
// next-intl echoes keys (and ignores interpolation params) so we assert on the state keys.
|
||||
jest.mock('next-intl', () => ({
|
||||
useTranslations: () => (key: string) => key,
|
||||
}));
|
||||
|
||||
import DocumentUpload from './DocumentUpload';
|
||||
|
||||
function renderUpload(props: Partial<React.ComponentProps<typeof DocumentUpload>> = {}) {
|
||||
const onUpload = props.onUpload ?? jest.fn().mockResolvedValue({ name: 'license.pdf' });
|
||||
const utils = render(
|
||||
<ThemeProvider>
|
||||
<DocumentUpload label="License" onUpload={onUpload} {...props} />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
const input = utils.container.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
return { ...utils, input, onUpload };
|
||||
}
|
||||
|
||||
function selectFile(input: HTMLInputElement, file: File) {
|
||||
Object.defineProperty(input, 'files', { value: [file], configurable: true });
|
||||
fireEvent.change(input);
|
||||
}
|
||||
|
||||
describe('<DocumentUpload/> component', () => {
|
||||
it('renders the field label and the idle choose zone', () => {
|
||||
const { container } = renderUpload();
|
||||
expect(screen.getByText('License')).toBeInTheDocument();
|
||||
expect(container.querySelector('[data-upload-state="idle"]')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('rejects a disallowed file type before uploading', () => {
|
||||
const { input, onUpload, container } = renderUpload();
|
||||
selectFile(input, new File(['x'], 'note.txt', { type: 'text/plain' }));
|
||||
expect(onUpload).not.toHaveBeenCalled();
|
||||
expect(container.querySelector('[data-upload-state="error"]')).toBeInTheDocument();
|
||||
expect(screen.getByText('upload_bad_type')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('rejects a file over the size cap before uploading', () => {
|
||||
const { input, onUpload } = renderUpload({ maxSizeBytes: 10 });
|
||||
const big = new File([new Uint8Array(50)], 'card.png', { type: 'image/png' });
|
||||
selectFile(input, big);
|
||||
expect(onUpload).not.toHaveBeenCalled();
|
||||
expect(screen.getByText('upload_too_large')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('uploads a valid file and shows the success state', async () => {
|
||||
const onUploaded = jest.fn();
|
||||
const { input, onUpload } = renderUpload({ onUploaded });
|
||||
selectFile(input, new File(['%PDF'], 'license.pdf', { type: 'application/pdf' }));
|
||||
await waitFor(() => expect(screen.getByText('upload_success')).toBeInTheDocument());
|
||||
expect(onUpload).toHaveBeenCalledTimes(1);
|
||||
expect(onUploaded).toHaveBeenCalledWith({ name: 'license.pdf' });
|
||||
});
|
||||
|
||||
it('shows a retryable error when the upload fails', async () => {
|
||||
const onUpload = jest.fn().mockRejectedValue(new Error('boom'));
|
||||
const { input, container } = renderUpload({ onUpload });
|
||||
selectFile(input, new File(['%PDF'], 'license.pdf', { type: 'application/pdf' }));
|
||||
await waitFor(() => expect(container.querySelector('[data-upload-state="error"]')).toBeInTheDocument());
|
||||
expect(screen.getByText('upload_retry')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the rejected state with its reason and a re-upload affordance', () => {
|
||||
const { container } = renderUpload({ rejected: true, rejectionReason: 'Blurry scan' });
|
||||
expect(container.querySelector('[data-upload-state="rejected"]')).toBeInTheDocument();
|
||||
expect(screen.getByText('Blurry scan')).toBeInTheDocument();
|
||||
expect(screen.getByText('upload_reupload')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders an already-uploaded document from server metadata', () => {
|
||||
const { container } = renderUpload({ existingDoc: { name: 'prior-license.pdf' } });
|
||||
expect(container.querySelector('[data-upload-state="success"]')).toBeInTheDocument();
|
||||
expect(screen.getByText('prior-license.pdf')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,313 @@
|
||||
'use client';
|
||||
import { ChangeEvent, FunctionComponent, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Box from '@mui/material/Box';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import AppButton from '../common/AppButton';
|
||||
import AppIcon from '../common/AppIcon';
|
||||
import { ACCEPTED_DOCUMENT_TYPES, MAX_DOCUMENT_SIZE_BYTES } from '@/services/verification/constants';
|
||||
|
||||
/** The stored document as the uploader displays it — a name + optional size, never bytes. */
|
||||
export interface UploadedDocInfo {
|
||||
name: string;
|
||||
sizeBytes?: number;
|
||||
}
|
||||
|
||||
type UploadState = 'idle' | 'uploading' | 'success' | 'error';
|
||||
|
||||
export interface DocumentUploadProps {
|
||||
/** Field label (already translated). */
|
||||
label: string;
|
||||
/** Optional helper text under the label. */
|
||||
hint?: string;
|
||||
/** Accepted MIME types. Defaults to jpg/png/pdf (the b6 object-storage limits). */
|
||||
accept?: readonly string[];
|
||||
/** Max file size in bytes. Defaults to 5 MB. */
|
||||
maxSizeBytes?: number;
|
||||
/** Mobile camera hint: `environment` (rear — ID card) or `user` (front — selfie). */
|
||||
capture?: 'user' | 'environment';
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* The async upload action — receives the file + a progress reporter (0–100) and resolves with the
|
||||
* stored doc. For a server step this calls the verification seam; for a local capture (B4 identity) it
|
||||
* validates + resolves locally without a round-trip.
|
||||
*/
|
||||
onUpload: (file: File, onProgress: (percent: number) => void) => Promise<UploadedDocInfo>;
|
||||
/** Called after a successful upload with the resolved metadata. */
|
||||
onUploaded?: (doc: UploadedDocInfo) => void;
|
||||
/** A previously-uploaded document (drives the "already uploaded ✓" state from server metadata). */
|
||||
existingDoc?: UploadedDocInfo | null;
|
||||
/** When the step was rejected — renders the reason and a re-upload affordance (never a dead end). */
|
||||
rejected?: boolean;
|
||||
rejectionReason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable document uploader for every verification document step (national-ID card, license, education
|
||||
* cert, criminal record). Owns the full state machine — idle → validating → uploading (progress %) →
|
||||
* success (✓ + file name / local image preview) → error (retry) — with client-side type/size validation
|
||||
* before any upload and a re-upload affordance on reject. Returns the server's stored **metadata** only;
|
||||
* a local image preview never becomes the source of the "uploaded" truth. Its own chrome strings come
|
||||
* from the `verification` namespace; the caller passes the field `label`/`hint`.
|
||||
* @component DocumentUpload
|
||||
*/
|
||||
const DocumentUpload: FunctionComponent<DocumentUploadProps> = ({
|
||||
label,
|
||||
hint,
|
||||
accept = ACCEPTED_DOCUMENT_TYPES,
|
||||
maxSizeBytes = MAX_DOCUMENT_SIZE_BYTES,
|
||||
capture,
|
||||
disabled = false,
|
||||
onUpload,
|
||||
onUploaded,
|
||||
existingDoc = null,
|
||||
rejected = false,
|
||||
rejectionReason,
|
||||
}) => {
|
||||
const t = useTranslations('verification');
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [state, setState] = useState<UploadState>('idle');
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [errorKey, setErrorKey] = useState<string | null>(null);
|
||||
const [fileName, setFileName] = useState<string | null>(null);
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||
|
||||
// A local image preview is an object URL — revoke it when it changes or the component unmounts.
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (previewUrl) URL.revokeObjectURL(previewUrl);
|
||||
};
|
||||
}, [previewUrl]);
|
||||
|
||||
const openPicker = () => inputRef.current?.click();
|
||||
|
||||
const onFileSelected = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
event.target.value = ''; // allow re-picking the same file after an error
|
||||
if (!file) return;
|
||||
|
||||
if (!accept.includes(file.type)) {
|
||||
setState('error');
|
||||
setErrorKey('upload_bad_type');
|
||||
return;
|
||||
}
|
||||
if (file.size > maxSizeBytes) {
|
||||
setState('error');
|
||||
setErrorKey('upload_too_large');
|
||||
return;
|
||||
}
|
||||
|
||||
setErrorKey(null);
|
||||
setFileName(file.name);
|
||||
if (file.type.startsWith('image/')) {
|
||||
setPreviewUrl((prev) => {
|
||||
if (prev) URL.revokeObjectURL(prev);
|
||||
return URL.createObjectURL(file);
|
||||
});
|
||||
}
|
||||
setProgress(0);
|
||||
setState('uploading');
|
||||
|
||||
try {
|
||||
const doc = await onUpload(file, setProgress);
|
||||
setFileName(doc.name);
|
||||
setState('success');
|
||||
onUploaded?.(doc);
|
||||
} catch {
|
||||
// 401/403/5xx are already toasted by the fetch layer; show a retryable inline error here.
|
||||
setState('error');
|
||||
setErrorKey('upload_error');
|
||||
}
|
||||
};
|
||||
|
||||
const megabytes = Math.round(maxSizeBytes / (1024 * 1024));
|
||||
const acceptAttr = accept.join(',');
|
||||
// The "already uploaded" resting state is driven by server metadata (existingDoc) or a just-completed
|
||||
// upload — never by retained bytes.
|
||||
const showUploaded = state === 'success' || (state === 'idle' && existingDoc != null);
|
||||
const uploadedName = fileName ?? existingDoc?.name ?? '';
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.75 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>
|
||||
{label}
|
||||
</Typography>
|
||||
{hint ? (
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
||||
{hint}
|
||||
</Typography>
|
||||
) : null}
|
||||
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept={acceptAttr}
|
||||
capture={capture}
|
||||
hidden
|
||||
disabled={disabled}
|
||||
onChange={onFileSelected}
|
||||
/>
|
||||
|
||||
{rejected ? (
|
||||
<Paper
|
||||
elevation={0}
|
||||
data-upload-state="rejected"
|
||||
sx={{
|
||||
p: 2,
|
||||
borderRadius: 2,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
borderInlineStartWidth: 4,
|
||||
borderInlineStartColor: 'var(--bal-error)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" sx={{ gap: 1, alignItems: 'center' }}>
|
||||
<AppIcon icon="rejected" size={20} color="var(--bal-error)" />
|
||||
<Typography variant="body2" sx={{ fontWeight: 700 }}>
|
||||
{t('upload_rejected')}
|
||||
</Typography>
|
||||
</Stack>
|
||||
{rejectionReason ? (
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||||
{rejectionReason}
|
||||
</Typography>
|
||||
) : null}
|
||||
<AppButton
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
startIcon="upload"
|
||||
onClick={openPicker}
|
||||
disabled={disabled}
|
||||
sx={{ m: 0, alignSelf: 'flex-start' }}
|
||||
>
|
||||
{t('upload_reupload')}
|
||||
</AppButton>
|
||||
</Paper>
|
||||
) : state === 'uploading' ? (
|
||||
<Paper elevation={0} data-upload-state="uploading" sx={uploadedSx}>
|
||||
<Stack direction="row" sx={{ gap: 1, alignItems: 'center' }}>
|
||||
<AppIcon icon="upload" size={20} color="var(--bal-primary)" />
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, flexGrow: 1, wordBreak: 'break-all' }}>
|
||||
{fileName}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
||||
{progress}%
|
||||
</Typography>
|
||||
</Stack>
|
||||
<LinearProgress variant="determinate" value={progress} sx={{ borderRadius: 1, height: 6 }} />
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
||||
{t('upload_uploading')}
|
||||
</Typography>
|
||||
</Paper>
|
||||
) : showUploaded ? (
|
||||
<Paper elevation={0} data-upload-state="success" sx={uploadedSx}>
|
||||
<Stack direction="row" sx={{ gap: 1.5, alignItems: 'center' }}>
|
||||
{previewUrl ? (
|
||||
<Box
|
||||
component="img"
|
||||
src={previewUrl}
|
||||
alt=""
|
||||
sx={{ width: 48, height: 48, objectFit: 'cover', borderRadius: 1.5, flexShrink: 0 }}
|
||||
/>
|
||||
) : (
|
||||
<AppIcon icon="document" size={28} color="var(--bal-primary)" />
|
||||
)}
|
||||
<Stack sx={{ gap: 0.25, flexGrow: 1, minWidth: 0 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, wordBreak: 'break-all' }}>
|
||||
{uploadedName}
|
||||
</Typography>
|
||||
<Stack direction="row" sx={{ gap: 0.5, alignItems: 'center' }}>
|
||||
<AppIcon icon="verified" size={16} color="var(--bal-success)" />
|
||||
<Typography variant="caption" sx={{ color: 'var(--bal-success)' }}>
|
||||
{t('upload_success')}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<AppButton variant="text" color="primary" onClick={openPicker} disabled={disabled} sx={{ m: 0 }}>
|
||||
{t('upload_change')}
|
||||
</AppButton>
|
||||
</Stack>
|
||||
</Paper>
|
||||
) : state === 'error' ? (
|
||||
<Paper
|
||||
elevation={0}
|
||||
data-upload-state="error"
|
||||
sx={{ ...uploadedSx, borderInlineStartWidth: 4, borderInlineStartColor: 'var(--bal-error)' }}
|
||||
>
|
||||
<Stack direction="row" sx={{ gap: 1, alignItems: 'center' }}>
|
||||
<AppIcon icon="error" size={20} color="var(--bal-error)" />
|
||||
<Typography variant="body2" sx={{ color: 'var(--bal-error)', flexGrow: 1 }}>
|
||||
{t(errorKey ?? 'upload_error', { size: megabytes })}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<AppButton
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
startIcon="refresh"
|
||||
onClick={openPicker}
|
||||
disabled={disabled}
|
||||
sx={{ m: 0, alignSelf: 'flex-start' }}
|
||||
>
|
||||
{t('upload_retry')}
|
||||
</AppButton>
|
||||
</Paper>
|
||||
) : (
|
||||
<Box
|
||||
role="button"
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
data-upload-state="idle"
|
||||
onClick={disabled ? undefined : openPicker}
|
||||
onKeyDown={(event) => {
|
||||
if (!disabled && (event.key === 'Enter' || event.key === ' ')) {
|
||||
event.preventDefault();
|
||||
openPicker();
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
p: 3,
|
||||
borderRadius: 2,
|
||||
border: '1px dashed',
|
||||
borderColor: 'divider',
|
||||
textAlign: 'center',
|
||||
cursor: disabled ? 'default' : 'pointer',
|
||||
opacity: disabled ? 0.6 : 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 0.75,
|
||||
transition: 'border-color 120ms',
|
||||
'&:hover': disabled ? undefined : { borderColor: 'var(--bal-primary)' },
|
||||
}}
|
||||
>
|
||||
<AppIcon icon={capture ? 'camera' : 'upload'} size={28} color="var(--bal-primary)" />
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
{capture ? t('upload_capture') : t('upload_choose')}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
||||
{t('upload_size_hint', { size: megabytes })}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
// Shared card styling for the uploading / success / error states.
|
||||
const uploadedSx = {
|
||||
p: 2,
|
||||
borderRadius: 2,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 1,
|
||||
} as const;
|
||||
|
||||
export default DocumentUpload;
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from './DocumentUpload';
|
||||
export type { DocumentUploadProps, UploadedDocInfo } from './DocumentUpload';
|
||||
@@ -0,0 +1,38 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ThemeProvider } from '../../theme';
|
||||
|
||||
// next-intl echoes keys so we assert on the label key each state maps to.
|
||||
jest.mock('next-intl', () => ({
|
||||
useTranslations: () => (key: string) => key,
|
||||
}));
|
||||
|
||||
import TrustBadge from './TrustBadge';
|
||||
import type { BadgeState } from '@/services/verification/types';
|
||||
|
||||
function renderBadge(state: BadgeState) {
|
||||
return render(
|
||||
<ThemeProvider>
|
||||
<TrustBadge state={state} />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('<TrustBadge/> component', () => {
|
||||
it('renders the verified label + a data attribute for the verified state', () => {
|
||||
const { container } = renderBadge('verified');
|
||||
expect(screen.getByText('badge_verified')).toBeInTheDocument();
|
||||
expect(container.querySelector('[data-badge-state="verified"]')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the unverified state distinctly (neutral, not alarming)', () => {
|
||||
const { container } = renderBadge('unverified');
|
||||
expect(screen.getByText('badge_unverified')).toBeInTheDocument();
|
||||
expect(container.querySelector('[data-badge-state="unverified"]')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders expired as its own state, distinct from unverified', () => {
|
||||
const { container } = renderBadge('expired');
|
||||
expect(screen.getByText('badge_expired')).toBeInTheDocument();
|
||||
expect(container.querySelector('[data-badge-state="expired"]')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import { FunctionComponent } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Chip, { ChipProps } from '@mui/material/Chip';
|
||||
import AppIcon from '../common/AppIcon';
|
||||
import type { BadgeState } from '@/services/verification/types';
|
||||
|
||||
interface BadgeStyle {
|
||||
bg: string;
|
||||
fg: string;
|
||||
icon: string;
|
||||
labelKey: string;
|
||||
}
|
||||
|
||||
// Colors resolve from the semantic --bal-* tokens so the badge switches with the color scheme.
|
||||
// verified = green trust mark; unverified = neutral (never alarming); expired = amber "needs renewal"
|
||||
// (distinct from unverified — a required credential lapsed). Never a hard-coded hex.
|
||||
const BADGE_STYLE: Record<BadgeState, BadgeStyle> = {
|
||||
verified: { bg: 'var(--bal-success)', fg: 'var(--bal-success-contrast)', icon: 'verified', labelKey: 'badge_verified' },
|
||||
unverified: { bg: 'var(--bal-divider)', fg: 'var(--bal-text-secondary)', icon: 'info', labelKey: 'badge_unverified' },
|
||||
expired: { bg: 'var(--bal-warning)', fg: 'var(--bal-warning-contrast)', icon: 'warning', labelKey: 'badge_expired' },
|
||||
};
|
||||
|
||||
export interface TrustBadgeProps extends Omit<ChipProps, 'color' | 'icon' | 'label'> {
|
||||
/** The trust state — verified / unverified / expired. */
|
||||
state: BadgeState;
|
||||
}
|
||||
|
||||
/**
|
||||
* The public trust signal (the "✓ تاییدشده" mark) rendered on a nurse's profile and — reused unchanged
|
||||
* in f6 — on search results and the public nurse profile. Fed by `GetVerifiedBadgeQuery`; the state is
|
||||
* derived by the caller (`ownBadgeState`/`publicBadgeState`). Honest by construction: `verified` only
|
||||
* renders when the aggregate is approved; `expired` is visually distinct from never-verified.
|
||||
* @component TrustBadge
|
||||
*/
|
||||
const TrustBadge: FunctionComponent<TrustBadgeProps> = ({ state, size = 'small', sx, ...rest }) => {
|
||||
const t = useTranslations('verification');
|
||||
const style = BADGE_STYLE[state];
|
||||
return (
|
||||
<Chip
|
||||
data-badge-state={state}
|
||||
size={size}
|
||||
label={t(style.labelKey)}
|
||||
icon={<AppIcon icon={style.icon} size={16} color={style.fg} />}
|
||||
sx={{ backgroundColor: style.bg, color: style.fg, fontWeight: 700, ...sx }}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrustBadge;
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from './TrustBadge';
|
||||
export type { TrustBadgeProps } from './TrustBadge';
|
||||
@@ -48,6 +48,13 @@ import PostSurgeryIcon from '@mui/icons-material/HealingOutlined';
|
||||
import InfantIcon from '@mui/icons-material/ChildCareOutlined';
|
||||
import ChronicIcon from '@mui/icons-material/MonitorHeartOutlined';
|
||||
import CompanionshipIcon from '@mui/icons-material/VolunteerActivismOutlined';
|
||||
// Verification — nurse trust flow (f5/b6): document upload, credential + identity, re-upload
|
||||
import UploadIcon from '@mui/icons-material/CloudUploadOutlined';
|
||||
import DocumentIcon from '@mui/icons-material/InsertDriveFileOutlined';
|
||||
import RefreshIcon from '@mui/icons-material/RefreshOutlined';
|
||||
import IdentityIcon from '@mui/icons-material/BadgeOutlined';
|
||||
import LicenseIcon from '@mui/icons-material/WorkspacePremiumOutlined';
|
||||
import PublishIcon from '@mui/icons-material/RocketLaunchOutlined';
|
||||
|
||||
/**
|
||||
* List of all available Icon names
|
||||
@@ -110,4 +117,10 @@ export const ICONS /* Note: Setting type disables property autocomplete :( was -
|
||||
infant: InfantIcon,
|
||||
chronic: ChronicIcon,
|
||||
companionship: CompanionshipIcon,
|
||||
upload: UploadIcon,
|
||||
document: DocumentIcon,
|
||||
refresh: RefreshIcon,
|
||||
identity: IdentityIcon,
|
||||
license: LicenseIcon,
|
||||
publish: PublishIcon,
|
||||
};
|
||||
|
||||
@@ -15,6 +15,8 @@ import BankStatusPanel from './BankStatusPanel';
|
||||
import CategoryTile from './CategoryTile';
|
||||
import PriceDisplay from './PriceDisplay';
|
||||
import VariantCard from './VariantCard';
|
||||
import TrustBadge from './TrustBadge';
|
||||
import DocumentUpload from './DocumentUpload';
|
||||
|
||||
export {
|
||||
UserInfo,
|
||||
@@ -32,6 +34,8 @@ export {
|
||||
CategoryTile,
|
||||
PriceDisplay,
|
||||
VariantCard,
|
||||
TrustBadge,
|
||||
DocumentUpload,
|
||||
};
|
||||
export type { PlaceholderScreenProps } from './PlaceholderScreen';
|
||||
export type { OtpInputProps } from './OtpInput';
|
||||
@@ -47,3 +51,5 @@ export type { BankStatusPanelProps } from './BankStatusPanel';
|
||||
export type { CategoryTileProps } from './CategoryTile';
|
||||
export type { PriceDisplayProps } from './PriceDisplay';
|
||||
export type { VariantCardProps } from './VariantCard';
|
||||
export type { TrustBadgeProps } from './TrustBadge';
|
||||
export type { DocumentUploadProps, UploadedDocInfo } from './DocumentUpload';
|
||||
|
||||
Reference in New Issue
Block a user