@homefile/components-v2 2.60.2 → 2.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/locales/en/index.json +5 -3
- package/dist/components/folderPanel/folderDetails/FilesUploader.js +1 -0
- package/dist/components/notifications/NotificationCard.js +13 -3
- package/dist/components/onboarding/pages/EmailValidation.js +1 -1
- package/dist/components/onboarding/pages/NewPassword.d.ts +1 -1
- package/dist/components/onboarding/pages/NewPassword.js +12 -10
- package/dist/components/onboarding/pages/ResendResetPassword.js +1 -1
- package/dist/hooks/folderPanel/useFilesUploader.d.ts +2 -1
- package/dist/hooks/folderPanel/useFilesUploader.js +12 -8
- package/dist/interfaces/pages/NewPassword.interface.d.ts +7 -0
- package/dist/stories/onboarding/pages/NewPassword.stories.d.ts +19 -0
- package/dist/stories/onboarding/pages/NewPassword.stories.js +17 -0
- package/dist/utils/Formaters.utils.d.ts +4 -0
- package/dist/utils/Formaters.utils.js +10 -0
- package/package.json +1 -1
|
@@ -266,7 +266,8 @@
|
|
|
266
266
|
"placeholderDescription": "Enter a description",
|
|
267
267
|
"title": "Files",
|
|
268
268
|
"errorSize": "Please select a file less than 25MB",
|
|
269
|
-
"errorType": "File type must be .jpeg, .jpg, .pdf, .txt, .doc or .docx"
|
|
269
|
+
"errorType": "File type must be .jpeg, .jpg, .pdf, .txt, .doc or .docx",
|
|
270
|
+
"errorTypeWithSpreadsheets": "File type must be .jpeg, .jpg, .pdf, .txt, .doc, .docx, .xlsx, .xls or .csv"
|
|
270
271
|
},
|
|
271
272
|
"fileThumbnail": "File Thumbnail",
|
|
272
273
|
"filters": {
|
|
@@ -688,7 +689,8 @@
|
|
|
688
689
|
"newPassword": {
|
|
689
690
|
"confirm": "Confirm Password",
|
|
690
691
|
"create": "Create Password",
|
|
691
|
-
"title": "Please enter your new password."
|
|
692
|
+
"title": "Please enter your new password.",
|
|
693
|
+
"support": "Still stuck? Email"
|
|
692
694
|
},
|
|
693
695
|
"notifications": {
|
|
694
696
|
"noNotifications": "You currently have no notifications.",
|
|
@@ -1131,7 +1133,7 @@
|
|
|
1131
1133
|
"signin": {
|
|
1132
1134
|
"forgotBt": "Forgot Password?",
|
|
1133
1135
|
"title": "Please Log in",
|
|
1134
|
-
"titleSocial": "Please login
|
|
1136
|
+
"titleSocial": "Please login",
|
|
1135
1137
|
"signinBt": "Log in",
|
|
1136
1138
|
"signupBt": "Sign up",
|
|
1137
1139
|
"signup": "Don’t have a Homefile account?",
|
|
@@ -6,6 +6,7 @@ export const FilesUploader = ({ displayType, initialHeight, onUpload, totalFiles
|
|
|
6
6
|
const { acceptedFiles, errorMessage, getInputProps, getRootProps } = useFilesUploader({
|
|
7
7
|
onUpload,
|
|
8
8
|
uploading,
|
|
9
|
+
acceptSpreadsheets: true,
|
|
9
10
|
});
|
|
10
11
|
const isGrid = displayType === 'grid';
|
|
11
12
|
const hasFiles = totalFiles > 0;
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Container, Flex, IconButton, Stack, Text } from '@chakra-ui/react';
|
|
2
|
+
import { Box, Container, Flex, IconButton, Stack, Text } from '@chakra-ui/react';
|
|
3
3
|
import { Close } from '../../components';
|
|
4
|
-
import {
|
|
4
|
+
import { formatNotificationDate } from '../../utils';
|
|
5
|
+
// Matt's notification design (2026-10): a soft shadow, the title in Poppins
|
|
6
|
+
// semibold caps, "11:35 AM · Wed, Oct 12, 2024" in grey, the message in
|
|
7
|
+
// Assistant 16px / 20px line height, 20px padding under it, 13px left and
|
|
8
|
+
// right, a 16px close X, a 12px title (Matt, 2026-10-05). Every line height
|
|
9
|
+
// is at most font size + 4px (his rule): title 16px, date 14px, message 20px.
|
|
10
|
+
// Each kind keeps its own colour.
|
|
11
|
+
const DATE_GREY = '#707070';
|
|
12
|
+
const MESSAGE_COLOR = '#3E4045';
|
|
13
|
+
const CLOSE_BLUE = '#3C80BA';
|
|
5
14
|
export const NotificationCard = ({ _id, createdAt, label, message, onDismiss, status, }) => {
|
|
6
15
|
const bgColors = {
|
|
7
16
|
added: 'lightGreen.3',
|
|
@@ -15,5 +24,6 @@ export const NotificationCard = ({ _id, createdAt, label, message, onDismiss, st
|
|
|
15
24
|
shared: 'yellow.1',
|
|
16
25
|
updated: 'lightBlue.9',
|
|
17
26
|
};
|
|
18
|
-
|
|
27
|
+
const { time, day } = formatNotificationDate({ date: createdAt });
|
|
28
|
+
return (_jsx(Container, { pt: "2", pb: "20px", px: "13px", bg: bgColors[status], boxShadow: "0 3px 6px rgba(0, 0, 0, 0.16)", children: _jsxs(Stack, { spacing: "3", children: [_jsxs(Flex, { justify: "space-between", align: "center", children: [_jsx(Text, { fontSize: "12px", lineHeight: "16px", fontFamily: "primary", fontWeight: "semibold", textTransform: "uppercase", color: colors[status], children: label }), _jsxs(Flex, { gap: "2", align: "center", children: [_jsxs(Flex, { gap: "1.5", align: "center", children: [_jsx(Text, { variant: "date", lineHeight: "14px", color: DATE_GREY, children: time }), _jsx(Box, { "aria-hidden": true, w: "2px", h: "2px", borderRadius: "full", bg: DATE_GREY }), _jsx(Text, { variant: "date", lineHeight: "14px", color: DATE_GREY, children: day })] }), _jsx(IconButton, { variant: "ghost", "aria-label": "close", maxW: "fit-content", onClick: () => onDismiss(_id), icon: _jsx(Close, { size: 16, stroke: CLOSE_BLUE }) })] })] }), _jsx(Text, { fontSize: "16px", lineHeight: "20px", fontFamily: "secondary", color: MESSAGE_COLOR, children: message })] }) }));
|
|
19
29
|
};
|
|
@@ -4,5 +4,5 @@ import { chakra, Container, Text, Image, Center, Button, } from '@chakra-ui/reac
|
|
|
4
4
|
import { Logo, ButtonLoader, BasePageWrapper } from '../../../components';
|
|
5
5
|
import { Confirmation } from '../../../assets/images';
|
|
6
6
|
export const EmailValidation = ({ email, isLoading = false, handleResendBt, }) => {
|
|
7
|
-
return (_jsx(BasePageWrapper, { children: _jsxs(Container, { size: "validation", boxShadow: "lg", children: [_jsxs(Container, { variant: "secondary", children: [_jsx(Logo, {}), _jsxs(Text, { variant: "title", textAlign: "center", px: ['container.sm', 'container.xl'], pb: 6, minH: "6rem", children: [_jsx(chakra.span, { color: "gray.3", children: t('validation.title1') }), _jsx(chakra.span, { color: "
|
|
7
|
+
return (_jsx(BasePageWrapper, { children: _jsxs(Container, { size: "validation", boxShadow: "lg", children: [_jsxs(Container, { variant: "secondary", children: [_jsx(Logo, {}), _jsxs(Text, { variant: "title", textAlign: "center", px: ['container.sm', 'container.xl'], pb: 6, minH: "6rem", children: [_jsx(chakra.span, { color: "gray.3", children: t('validation.title1') }), _jsx(chakra.span, { color: "gray.3", children: email }), _jsx(chakra.span, { color: "gray.3", children: t('validation.title2') })] }), _jsx(Center, { children: _jsx(Image, { src: Confirmation, alt: "Activate account", h: ['100px', '145px'], w: "auto" }) })] }), _jsx(Text, { variant: "info", px: ['container.md', 'container.lg'], py: "2rem", textAlign: "center", children: t('validation.alert1') }), _jsx(Center, { children: _jsx(Button, { mb: 10, mx: 4, variant: "secondary", onClick: handleResendBt, isLoading: isLoading, spinner: _jsx(ButtonLoader, { isOutlined: true }), size: "onboarding", children: t('validation.resendBt') }) })] }) }));
|
|
8
8
|
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { NewPasswordI } from '../../../interfaces';
|
|
2
|
-
export declare const NewPassword: ({ isLoading, handleReset, userEmail, }: NewPasswordI) => import("react/jsx-runtime").JSX.Element;
|
|
2
|
+
export declare const NewPassword: ({ isLoading, handleReset, userEmail, errorMessage, errorActionText, handleErrorAction, supportEmail, }: NewPasswordI) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -4,15 +4,17 @@ import { Box, Button, Text, Container, Stack, chakra } from '@chakra-ui/react';
|
|
|
4
4
|
import { Logo, Footer, ButtonLoader, PasswordInput, BasePageWrapper, } from '../../../components';
|
|
5
5
|
import { isValidPassword, passwordMatch } from '../../../utils';
|
|
6
6
|
import { useNewPassword } from '../../../hooks';
|
|
7
|
-
export const NewPassword = ({ isLoading, handleReset, userEmail, }) => {
|
|
7
|
+
export const NewPassword = ({ isLoading, handleReset, userEmail, errorMessage, errorActionText, handleErrorAction, supportEmail, }) => {
|
|
8
8
|
const { inputs, handleInputChange, handleSubmit, isValidated } = useNewPassword(handleReset);
|
|
9
|
-
return (_jsxs(BasePageWrapper, { children: [_jsxs(Container, { size: "onboarding", boxShadow: "lg", children: [_jsx(Logo, {}), _jsx(Container, { variant: "ghost", mt: 4, mb: 6, children: _jsxs(Text, { variant: "title", textAlign: "center", px: [10, 0], noOfLines: [2, 1], children: [_jsx(chakra.span, { color: "gray.3", children: t('welcome.greeting2') }), _jsx(chakra.span, { color: "
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
return (_jsxs(BasePageWrapper, { children: [_jsxs(Container, { size: "onboarding", boxShadow: "lg", children: [_jsx(Logo, {}), _jsx(Container, { variant: "ghost", mt: 4, mb: 6, children: _jsxs(Text, { variant: "title", textAlign: "center", px: [10, 0], noOfLines: [2, 1], children: [_jsx(chakra.span, { color: "gray.3", children: t('welcome.greeting2') }), _jsx(chakra.span, { color: "gray.3", children: userEmail }), _jsx(chakra.span, { color: "gray.3", children: "," })] }) }), _jsx(Box, { px: ['container.sm', 'container.md', 'container.lg'], pb: "container.lg", mb: 6, children: _jsxs(Stack, { spacing: 8, children: [_jsxs(Stack, { spacing: errorMessage ? 'base' : 8, children: [_jsx(Text, { textAlign: "center", variant: "info", children: t('newPassword.title') }), Boolean(errorMessage) && (
|
|
10
|
+
// Faint magenta box behind the whole message (Matt, 2026-10-06).
|
|
11
|
+
_jsxs(Stack, { spacing: 2, align: "center", bg: "status.errorBg", borderRadius: "sm", px: 4, py: 3, children: [_jsx(Text, { variant: "error", textAlign: "center", children: errorMessage }), Boolean(errorActionText) && handleErrorAction && (_jsx(Button, { variant: "text", fontSize: "xs", lineHeight: "13px", h: "auto", onClick: handleErrorAction, children: errorActionText })), Boolean(supportEmail) && (_jsxs(Text, { textAlign: "center", variant: "hint", children: [t('newPassword.support'), ' ', _jsx("a", { href: `mailto:${supportEmail}`, children: supportEmail })] }))] })), _jsxs(Stack, { spacing: 3, children: [_jsx(PasswordInput, { errorMessage: !isValidPassword(inputs.password)
|
|
12
|
+
? t('forms.errorPasswordLength')
|
|
13
|
+
: t('forms.errorPasswordMatch'), hasError: isValidated &&
|
|
14
|
+
(!isValidPassword(inputs.password) ||
|
|
15
|
+
!passwordMatch(inputs.password, inputs.confirmPassword)), id: "password", placeholder: t('newPassword.create'), value: inputs.password || '', handleChange: handleInputChange }), _jsx(PasswordInput, { errorMessage: !isValidPassword(inputs.confirmPassword)
|
|
16
|
+
? t('forms.errorPasswordLength')
|
|
17
|
+
: t('forms.errorPasswordMatch'), hasError: isValidated &&
|
|
18
|
+
(!isValidPassword(inputs.confirmPassword) ||
|
|
19
|
+
!passwordMatch(inputs.password, inputs.confirmPassword)), id: "confirmPassword", placeholder: t('newPassword.confirm'), value: inputs.confirmPassword || '', handleChange: handleInputChange })] })] }), _jsx(Button, { isLoading: isLoading, spinner: _jsx(ButtonLoader, {}), onClick: handleSubmit, children: t('reset.resetBt') })] }) })] }), _jsx(Footer, {})] }));
|
|
18
20
|
};
|
|
@@ -4,5 +4,5 @@ import { chakra, Container, Text, Image, Center, Button, } from '@chakra-ui/reac
|
|
|
4
4
|
import { BasePageWrapper, ButtonLoader, Logo } from '../../../components';
|
|
5
5
|
import { ResendPassword } from '../../../assets/images';
|
|
6
6
|
export const ResendResetPassword = ({ email, isLoading = false, handleResendBt, handleBack }) => {
|
|
7
|
-
return (_jsx(BasePageWrapper, { children: _jsxs(Container, { size: "validation", boxShadow: "lg", children: [_jsxs(Container, { variant: "secondary", children: [_jsx(Logo, {}), _jsxs(Text, { variant: "title", textAlign: "center", px: ['container.sm', 'container.xl'], pb: 6, minH: "6rem", children: [_jsx(chakra.span, { color: "gray.3", children: t('validation.title1') }), _jsx(chakra.span, { color: "
|
|
7
|
+
return (_jsx(BasePageWrapper, { children: _jsxs(Container, { size: "validation", boxShadow: "lg", children: [_jsxs(Container, { variant: "secondary", children: [_jsx(Logo, {}), _jsxs(Text, { variant: "title", textAlign: "center", px: ['container.sm', 'container.xl'], pb: 6, minH: "6rem", children: [_jsx(chakra.span, { color: "gray.3", children: t('validation.title1') }), _jsx(chakra.span, { color: "gray.3", children: email }), _jsx(Text, { variant: "title", children: t('validation.title3') })] }), _jsx(Center, { children: _jsx(Image, { src: ResendPassword, alt: "Activate account", h: ['100px', '145px'], w: "auto" }) })] }), _jsx(Text, { variant: "info", px: ['container.md', 'container.lg'], py: "2rem", textAlign: "center", children: t('validation.alert1') }), _jsx(Center, { children: _jsx(Button, { mb: handleBack ? 5 : 10, mx: 4, variant: "secondary", onClick: handleResendBt, isLoading: isLoading, spinner: _jsx(ButtonLoader, { isOutlined: true }), size: "onboarding", children: t('validation.resendResetBt') }) }), handleBack && (_jsx(Center, { children: _jsx(Button, { mb: 10, mx: 4, variant: "primary", onClick: handleBack, isLoading: isLoading, spinner: _jsx(ButtonLoader, { isOutlined: true }), size: "onboarding", children: t('validation.backToLogin') }) }))] }) }));
|
|
8
8
|
};
|
|
@@ -3,8 +3,9 @@ interface FilesUploaderHookI {
|
|
|
3
3
|
onUpload?: (files: FolderFileI[]) => void;
|
|
4
4
|
uploading: boolean;
|
|
5
5
|
displayImages?: string[];
|
|
6
|
+
acceptSpreadsheets?: boolean;
|
|
6
7
|
}
|
|
7
|
-
export declare const useFilesUploader: ({ onUpload, uploading, displayImages, }: FilesUploaderHookI) => {
|
|
8
|
+
export declare const useFilesUploader: ({ onUpload, uploading, displayImages, acceptSpreadsheets, }: FilesUploaderHookI) => {
|
|
8
9
|
acceptedFiles: FolderFileI[];
|
|
9
10
|
errorMessage: string;
|
|
10
11
|
getRootProps: <T extends import("react-dropzone").DropzoneRootProps>(props?: T) => T;
|
|
@@ -2,19 +2,21 @@ import { handleMapFile } from '../../utils';
|
|
|
2
2
|
import { t } from 'i18next';
|
|
3
3
|
import { useEffect, useState } from 'react';
|
|
4
4
|
import { useDropzone } from 'react-dropzone';
|
|
5
|
-
|
|
5
|
+
const SPREADSHEETS = {
|
|
6
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': [
|
|
7
|
+
'.xlsx',
|
|
8
|
+
],
|
|
9
|
+
'application/vnd.ms-excel': ['.xls'],
|
|
10
|
+
'text/csv': ['.csv'],
|
|
11
|
+
};
|
|
12
|
+
export const useFilesUploader = ({ onUpload, uploading, displayImages = [], acceptSpreadsheets = false, }) => {
|
|
6
13
|
const [isUploading, setIsUploading] = useState(false);
|
|
7
14
|
const [thumbnails, setThumbnails] = useState(displayImages);
|
|
8
15
|
const [acceptedFiles, setAcceptedFiles] = useState([]);
|
|
9
16
|
const [errorMessage, setErrorMessage] = useState('');
|
|
10
17
|
const hasError = errorMessage !== '';
|
|
11
18
|
const { getRootProps, getInputProps } = useDropzone({
|
|
12
|
-
accept: {
|
|
13
|
-
'image/*': ['.jpeg', '.jpg', '.pdf', '.txt', '.doc', '.docx', '.heic'],
|
|
14
|
-
'video/mp4': ['.mp4', '.MP4'],
|
|
15
|
-
'video/mov': ['.mov', '.MOV'],
|
|
16
|
-
'video/quicktime': ['.mov', '.MOV'],
|
|
17
|
-
},
|
|
19
|
+
accept: Object.assign({ 'image/*': ['.jpeg', '.jpg', '.pdf', '.txt', '.doc', '.docx', '.heic'], 'video/mp4': ['.mp4', '.MP4'], 'video/mov': ['.mov', '.MOV'], 'video/quicktime': ['.mov', '.MOV'] }, (acceptSpreadsheets ? SPREADSHEETS : {})),
|
|
18
20
|
disabled: isUploading,
|
|
19
21
|
maxSize: 25000000,
|
|
20
22
|
onDrop: (uploadedFiles) => {
|
|
@@ -24,7 +26,9 @@ export const useFilesUploader = ({ onUpload, uploading, displayImages = [], }) =
|
|
|
24
26
|
onDropRejected: (rejectFiles) => {
|
|
25
27
|
setErrorMessage(rejectFiles[0].errors[0].code === 'file-too-large'
|
|
26
28
|
? t('folderSharing.details.errorSize')
|
|
27
|
-
: t(
|
|
29
|
+
: t(acceptSpreadsheets
|
|
30
|
+
? 'folderSharing.details.errorTypeWithSpreadsheets'
|
|
31
|
+
: 'folderSharing.details.errorType'));
|
|
28
32
|
},
|
|
29
33
|
});
|
|
30
34
|
useEffect(() => {
|
|
@@ -6,4 +6,11 @@ export interface NewPasswordI {
|
|
|
6
6
|
isLoading: boolean;
|
|
7
7
|
handleReset: (form: NewPasswordFormI) => void;
|
|
8
8
|
userEmail: string;
|
|
9
|
+
/** Shown above the password fields, in the same error style as sign-in. */
|
|
10
|
+
errorMessage?: string;
|
|
11
|
+
/** Optional button under the error line (for example "Send a new link"). */
|
|
12
|
+
errorActionText?: string;
|
|
13
|
+
handleErrorAction?: () => void;
|
|
14
|
+
/** Optional support address under the error line, shown as an email link. */
|
|
15
|
+
supportEmail?: string;
|
|
9
16
|
}
|
|
@@ -9,3 +9,22 @@ export declare const NewPasswordPage: {
|
|
|
9
9
|
userEmail: string;
|
|
10
10
|
};
|
|
11
11
|
};
|
|
12
|
+
export declare const NewPasswordLinkExpired: {
|
|
13
|
+
(args: NewPasswordI): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
args: {
|
|
15
|
+
isLoading: boolean;
|
|
16
|
+
userEmail: string;
|
|
17
|
+
errorMessage: string;
|
|
18
|
+
errorActionText: string;
|
|
19
|
+
handleErrorAction: () => void;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
export declare const NewPasswordNoAnswer: {
|
|
23
|
+
(args: NewPasswordI): import("react/jsx-runtime").JSX.Element;
|
|
24
|
+
args: {
|
|
25
|
+
isLoading: boolean;
|
|
26
|
+
userEmail: string;
|
|
27
|
+
errorMessage: string;
|
|
28
|
+
supportEmail: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
@@ -11,3 +11,20 @@ NewPasswordPage.args = {
|
|
|
11
11
|
isLoading: true,
|
|
12
12
|
userEmail: 'user@user.com',
|
|
13
13
|
};
|
|
14
|
+
// The reset link was refused (expired, already used, or too many tries).
|
|
15
|
+
export const NewPasswordLinkExpired = (args) => (_jsx(CloudsAnimation, { children: _jsx(NewPassword, Object.assign({}, args)) }));
|
|
16
|
+
NewPasswordLinkExpired.args = {
|
|
17
|
+
isLoading: false,
|
|
18
|
+
userEmail: 'user@user.com',
|
|
19
|
+
errorMessage: 'This link has expired.',
|
|
20
|
+
errorActionText: 'Send a new link',
|
|
21
|
+
handleErrorAction: () => { },
|
|
22
|
+
};
|
|
23
|
+
// The request got no answer (offline, or the server didn't respond).
|
|
24
|
+
export const NewPasswordNoAnswer = (args) => (_jsx(CloudsAnimation, { children: _jsx(NewPassword, Object.assign({}, args)) }));
|
|
25
|
+
NewPasswordNoAnswer.args = {
|
|
26
|
+
isLoading: false,
|
|
27
|
+
userEmail: 'user@user.com',
|
|
28
|
+
errorMessage: "We couldn't reach Homefile. Check your connection, then press Reset Password.",
|
|
29
|
+
supportEmail: 'support@homefile.com',
|
|
30
|
+
};
|
|
@@ -6,6 +6,10 @@ interface FormatDateI {
|
|
|
6
6
|
export declare const formatDate: ({ date, locale }: FormatDateI) => string;
|
|
7
7
|
export declare const formatDateWithAt: ({ date, locale, }: FormatDateI) => string;
|
|
8
8
|
export declare const formatLongDateWithAt: ({ date }: FormatDateI) => string;
|
|
9
|
+
export declare const formatNotificationDate: ({ date }: FormatDateI) => {
|
|
10
|
+
time: string;
|
|
11
|
+
day: string;
|
|
12
|
+
};
|
|
9
13
|
export declare const shortFormatDate: ({ date, locale, }: FormatDateI) => string;
|
|
10
14
|
export declare const monthAndYearFormatDate: ({ date, locale, }: FormatDateI) => string;
|
|
11
15
|
export declare const dayAndMonthFormatDate: ({ date, locale, }: FormatDateI) => string;
|
|
@@ -43,6 +43,16 @@ export const formatLongDateWithAt = ({ date }) => {
|
|
|
43
43
|
const timeFormat = format(parsedDate, 'h:mm a');
|
|
44
44
|
return `${dateFormat} @${timeFormat}`;
|
|
45
45
|
};
|
|
46
|
+
// Notification cards (Matt's design, 2026-10): "11:35 AM" · "Wed, Oct 12, 2024".
|
|
47
|
+
export const formatNotificationDate = ({ date }) => {
|
|
48
|
+
if (!date)
|
|
49
|
+
return { time: '', day: '' };
|
|
50
|
+
const parsedDate = new Date(date);
|
|
51
|
+
return {
|
|
52
|
+
time: format(parsedDate, 'h:mm a'),
|
|
53
|
+
day: format(parsedDate, 'EEE, MMM d, yyyy'),
|
|
54
|
+
};
|
|
55
|
+
};
|
|
46
56
|
export const shortFormatDate = ({ date, locale = 'en-US', }) => {
|
|
47
57
|
if (!date)
|
|
48
58
|
return '';
|