feat: Company branding preferences

This commit is contained in:
Ahmed Bouhuolia
2024-09-28 14:47:59 +02:00
parent b9506424d1
commit c5d7a2bfd8
8 changed files with 244 additions and 1 deletions

View File

@@ -1,5 +1,6 @@
// @ts-nocheck
import { useRef, useState } from 'react';
import clsx from 'classnames';
import { Button, Intent } from '@blueprintjs/core';
import { Icon, Stack } from '@/components';
import { Dropzone, DropzoneProps } from '@/components/Dropzone';
@@ -69,7 +70,7 @@ export function CompanyLogoUpload({
onReject={(files) => console.log('rejected files', files)}
maxSize={5 * 1024 ** 2}
accept={[MIME_TYPES.png, MIME_TYPES.jpeg]}
classNames={{ root: styles?.root, content: styles.dropzoneContent }}
classNames={{ root: clsx(styles?.root, classNames?.root), content: styles.dropzoneContent }}
activateOnClick={false}
openRef={openRef}
{...dropzoneProps}

View File

@@ -0,0 +1,6 @@
.fileUploadRoot{
width: 350px;
height: 140px;
}

View File

@@ -0,0 +1,35 @@
import React, { createContext, useContext, ReactNode } from 'react';
interface PreferencesBrandingContextType {}
const PreferencesBrandingContext =
createContext<PreferencesBrandingContextType>(
{} as PreferencesBrandingContextType,
);
interface PreferencesBrandingProviderProps {
children: ReactNode;
}
export const PreferencesBrandingBoot: React.FC<
PreferencesBrandingProviderProps
> = ({ children }) => {
const contextValue: PreferencesBrandingContextType = {};
return (
<PreferencesBrandingContext.Provider value={contextValue}>
{children}
</PreferencesBrandingContext.Provider>
);
};
export const usePreferencesBrandingBoot = () => {
const context = useContext(PreferencesBrandingContext);
if (context === undefined) {
throw new Error(
'usePreferencesBranding must be used within a PreferencesBrandingProvider',
);
}
return context;
};

View File

@@ -0,0 +1,82 @@
import React, { CSSProperties } from 'react';
import { Formik, Form, FormikHelpers } from 'formik';
import * as Yup from 'yup';
import { PreferencesBrandingFormValues } from './_types';
import { useUploadAttachments } from '@/hooks/query/attachments';
import { AppToaster } from '@/components';
import { Intent } from '@blueprintjs/core';
import { excludePrivateProps } from '@/utils';
const initialValues = {
logoKey: '',
logoUri: '',
primaryColor: '',
};
const validationSchema = Yup.object({
logoKey: Yup.string().optional(),
logoUri: Yup.string().optional(),
primaryColor: Yup.string().required('Primary color is required'),
});
interface PreferencesBrandingFormProps {
children: React.ReactNode;
}
export const PreferencesBrandingForm = ({
children,
}: PreferencesBrandingFormProps) => {
// Uploads the attachments.
const { mutateAsync: uploadAttachments } = useUploadAttachments({});
const handleSubmit = async (
values: PreferencesBrandingFormValues,
{ setSubmitting }: FormikHelpers<PreferencesBrandingFormValues>,
) => {
const _values = { ...values };
const handleError = (message: string) => {
AppToaster.show({ intent: Intent.DANGER, message });
setSubmitting(false);
};
// Start upload the company logo file if it is presented.
if (values._logoFile) {
const formData = new FormData();
const key = Date.now().toString();
formData.append('file', values._logoFile);
formData.append('internalKey', key);
try {
// @ts-expect-error
const uploadedAttachmentRes = await uploadAttachments(formData);
setSubmitting(false);
// Adds the attachment key to the values after finishing upload.
_values['_logoFile'] = uploadedAttachmentRes?.key;
} catch {
handleError('An error occurred while uploading company logo.');
setSubmitting(false);
return;
}
// Exclude all the private props that starts with _.
const excludedPrivateValues = excludePrivateProps(_values);
}
};
return (
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
<Form style={formStyle}>{children}</Form>
</Formik>
);
};
const formStyle: CSSProperties = {
display: 'flex',
flexDirection: 'column',
flex: 1,
};

View File

@@ -0,0 +1,76 @@
import { Button, Classes, Intent, Text } from '@blueprintjs/core';
import { useFormikContext } from 'formik';
import { FFormGroup, Group, Stack } from '@/components';
import { FColorInput } from '@/components/Forms/FColorInput';
import { CompanyLogoUpload } from '@/containers/ElementCustomize/components/CompanyLogoUpload';
import { PreferencesBrandingFormValues } from './_types';
import styles from './PreferencesBranding.module.scss';
export function PreferencesBrandingFormContent() {
return (
<Stack style={{ flex: '1' }}>
<FFormGroup name={'companyLogo'} label={'Company Logo'}>
<Group spacing={15} align={'left'}>
<BrandingCompanyLogoUpload />
<BrandingCompanyLogoDesc />
</Group>
</FFormGroup>
<FFormGroup
name={'primaryColor'}
label={'Primary Color'}
helperText={
'Note: These preferences will be applied across PDF and mail templates, including the customer payment page.'
}
>
<FColorInput name={'primaryColor'} />
</FFormGroup>
</Stack>
);
}
export function PreferencesBrandingFormFooter() {
const { isSubmitting } = useFormikContext();
return (
<Group style={{ padding: '12px 0', borderTop: '1px solid #e1e1e1' }}>
<Button intent={Intent.PRIMARY} type={'submit'} loading={isSubmitting}>
Submit
</Button>
</Group>
);
}
export function BrandingCompanyLogoUpload() {
const { setFieldValue, values } =
useFormikContext<PreferencesBrandingFormValues>();
return (
<CompanyLogoUpload
initialPreview={values?.logoUri}
onChange={(file) => {
const imageUrl = file ? URL.createObjectURL(file) : '';
setFieldValue('_logoFile', file);
setFieldValue('logoUri', imageUrl);
}}
classNames={{
root: styles.fileUploadRoot,
}}
/>
);
}
function BrandingCompanyLogoDesc() {
return (
<Stack spacing={10} style={{ fontSize: 12, paddingTop: 12, flex: 1 }}>
<Text className={Classes.TEXT_MUTED}>
This logo will be displayed in transaction PDFs and email notifications.
</Text>
<Text className={Classes.TEXT_MUTED}>
Preferred Image Dimensions: 240 × 240 pixels @ 72 DPI Maximum File Size:
1MB
</Text>
</Stack>
);
}

View File

@@ -0,0 +1,32 @@
// @ts-nocheck
import * as R from 'ramda';
import { useEffect } from 'react';
import { Stack } from '@/components';
import { PreferencesBrandingBoot } from './PreferencesBrandingBoot';
import { PreferencesBrandingForm } from './PreferencesBrandingForm';
import {
PreferencesBrandingFormContent,
PreferencesBrandingFormFooter,
} from './PreferencesBrandingFormContent';
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
function PreferencesBrandingPageRoot({ changePreferencesPageTitle }) {
useEffect(() => {
changePreferencesPageTitle('Branding');
}, [changePreferencesPageTitle]);
return (
<Stack
style={{ padding: '20px 40px 0', maxWidth: 900, width: '100%', flex: 1 }}
>
<PreferencesBrandingBoot>
<PreferencesBrandingForm>
<PreferencesBrandingFormContent />
<PreferencesBrandingFormFooter />
</PreferencesBrandingForm>
</PreferencesBrandingBoot>
</Stack>
);
}
export default R.compose(withDashboardActions)(PreferencesBrandingPageRoot);

View File

@@ -0,0 +1,6 @@
export interface PreferencesBrandingFormValues {
logoKey: string;
logoUri: string;
primaryColor: string;
_logoFile?: any;
}

View File

@@ -9,6 +9,11 @@ export const getPreferenceRoutes = () => [
component: lazy(() => import('@/containers/Preferences/General/General')),
exact: true,
},
{
path: `${BASE_URL}/branding`,
component: lazy(() => import('../containers/Preferences/Branding/PreferencesBrandingPage')),
exact: true,
},
{
path: `${BASE_URL}/users`,
component: lazy(() => import('../containers/Preferences/Users/Users')),