feat: wip invoice customize

This commit is contained in:
Ahmed Bouhuolia
2024-09-08 17:34:19 +02:00
parent e6bad27771
commit f5e9485a12
15 changed files with 287 additions and 36 deletions

View File

@@ -8,7 +8,8 @@
.colorPicker{
background-color: rgb(103, 114, 229);
border-radius: 3px;
height: 14px;
width: 14px;
height: 16px;
width: 16px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
cursor: pointer;
}

View File

@@ -1,29 +1,79 @@
import { useState } from 'react';
import clsx from 'classnames';
import {
IInputGroupProps,
InputGroup,
IPopoverProps,
Popover,
PopoverInteractionKind,
Position,
} from '@blueprintjs/core';
import { useState } from 'react';
import { HexColorPicker } from 'react-colorful';
import { useUncontrolled } from '@/hooks/useUncontrolled';
import { Box, BoxProps } from '@/components';
import styles from './ColorField.module.scss';
export function ColorField() {
const [color, setColor] = useState('#aabbcc');
export interface ColorFieldProps {
value?: string;
initialValue?: string;
onChange?: (value: string) => void;
popoverProps?: Partial<IPopoverProps>;
inputProps?: Partial<IInputGroupProps>;
pickerProps?: Partial<BoxProps>;
pickerWrapProps?: Partial<BoxProps>;
}
export function ColorField({
value,
initialValue,
onChange,
popoverProps,
inputProps,
pickerWrapProps,
pickerProps,
}: ColorFieldProps) {
const [_value, handleChange] = useUncontrolled({
value,
initialValue,
onChange,
finalValue: '',
});
const [isOpen, setIsOpen] = useState<boolean>(false);
const handleClose = () => {
setIsOpen(false);
};
return (
<Popover
content={<HexColorPicker color={color} onChange={setColor} />}
content={<HexColorPicker color={_value} onChange={handleChange} />}
position={Position.BOTTOM}
interactionKind={PopoverInteractionKind.CLICK}
modifiers={{
offset: { offset: '0, 4' },
}}
onClose={handleClose}
isOpen={isOpen}
minimal
{...popoverProps}
>
<InputGroup
leftElement={<div className={styles.colorPicker}></div>}
className={styles.field}
value={_value}
leftElement={
<Box
{...pickerWrapProps}
style={{ padding: 8, ...pickerWrapProps?.style }}
>
<Box
onClick={() => setIsOpen((oldValue) => !oldValue)}
style={{ backgroundColor: _value }}
className={clsx(styles.colorPicker, pickerProps?.className)}
{...pickerProps}
/>
</Box>
}
{...inputProps}
className={clsx(styles.field, inputProps?.className)}
/>
</Popover>
);

View File

@@ -1,9 +1,64 @@
import { ColorField } from './ColorField';
import React from 'react';
import { getIn, FieldConfig, FieldProps } from 'formik';
import { Intent } from '@blueprintjs/core';
import { Field } from '@blueprintjs-formik/core';
import { ColorField, ColorFieldProps } from './ColorField';
interface FColorFieldProps {
name: string;
interface ColorFieldInputGroupProps
extends Omit<FieldConfig, 'children' | 'component' | 'as' | 'value'>,
ColorFieldProps {}
export interface ColorFieldToInputProps
extends Omit<FieldProps, 'onChange'>,
ColorFieldProps {}
/**
* Transforms field props to input group props for ColorField.
* @param {ColorFieldToInputProps}
* @returns {ColorFieldProps}
*/
function fieldToColorFieldInputGroup({
field: { onBlur: onFieldBlur, onChange: onFieldChange, value, ...field },
form: { touched, errors, setFieldValue },
onChange,
...props
}: ColorFieldToInputProps): ColorFieldProps {
const fieldError = getIn(errors, field.name);
const showError = getIn(touched, field.name) && !!fieldError;
return {
inputProps: {
intent: showError ? Intent.DANGER : Intent.NONE,
},
value,
onChange:
onChange ??
function (value: string) {
setFieldValue(field.name, value);
},
...field,
...props,
};
}
export function FColorField({ name }: FColorFieldProps) {
return <ColorField />;
/**
* Transforms field props to input group props for ColorField.
* @param {ColorFieldToInputProps} props -
* @returns {JSX.Element}
*/
function ColorFieldToInputGroup({
...props
}: ColorFieldToInputProps): JSX.Element {
return <ColorField {...fieldToColorFieldInputGroup(props)} />;
}
/**
* Input group Blueprint component binded with Formik for ColorField.
* @param {ColorFieldInputGroupProps}
* @returns {JSX.Element}
*/
export function FColorInput({
...props
}: ColorFieldInputGroupProps): JSX.Element {
return <Field {...props} component={ColorFieldToInputGroup} />;
}

View File

@@ -9,7 +9,7 @@ export default function InvoiceCustomizeContent() {
return (
<Box className={Classes.DRAWER_BODY}>
<InvoiceCustomizeForm>
<Group spacing={0} align="flex-start">
<Group spacing={0} align="stretch">
<InvoiceCustomizeTabsControllerProvider>
<InvoiceCustomizeFields />
<InvoiceCustomizePreview />

View File

@@ -1,7 +1,6 @@
// @ts-nocheck
import React from 'react';
import * as R from 'ramda';
import { Drawer, DrawerSuspense } from '@/components';
import withDrawers from '@/containers/Drawer/withDrawers';
@@ -25,6 +24,7 @@ function InvoiceCustomizeDrawerRoot({
name={name}
size={'100%'}
>
<DrawerSuspense>
<InvoiceCustomizeContent />
</DrawerSuspense>

View File

@@ -4,4 +4,16 @@
.mainFields{
flex: 1;
}
.fieldGroup {
:global .bp4-form-content{
margin-left: auto;
}
}
.footerActions{
padding: 10px 16px;
border-top: 1px solid #d9d9d9;
flex-flow: row-reverse;
}

View File

@@ -1,9 +1,16 @@
import { Box, Group } from '@/components';
// @ts-nocheck
import * as R from 'ramda';
import { Box, Group, Stack } from '@/components';
import { InvoiceCustomizeHeader } from './InvoiceCustomizeHeader';
import { InvoiceCustomizeTabs } from './InvoiceCustomizeTabs';
import styles from './InvoiceCustomizeFields.module.scss';
import { InvoiceCustomizeGeneralField } from './InvoiceCustomizeGeneralFields';
import { useInvoiceCustomizeTabsController } from './InvoiceCustomizeTabsController';
import { Button, Intent } from '@blueprintjs/core';
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
import { useFormikContext } from 'formik';
import { InvoiceCustomizeContentFields } from './InvoiceCutomizeContentFields';
export function InvoiceCustomizeFields() {
return (
@@ -17,10 +24,40 @@ export function InvoiceCustomizeFields() {
export function InvoiceCustomizeFieldsMain() {
const { currentTabId } = useInvoiceCustomizeTabsController();
return (
<Box className={styles.mainFields}>
<Stack spacing={0} className={styles.mainFields}>
<InvoiceCustomizeHeader label={'Customize'} />
{currentTabId === 'general' && <InvoiceCustomizeGeneralField />}
</Box>
<Stack spacing={0} style={{ flex: '1 1 auto' }}>
{currentTabId === 'general' && <InvoiceCustomizeGeneralField />}
{currentTabId === 'content' && <InvoiceCustomizeContentFields />}
<InvoiceCustomizeFooterActions />
</Stack>
</Stack>
);
}
function InvoiceCustomizeFooterActionsRoot({ closeDrawer }) {
const { name } = useDrawerContext();
const { submitForm } = useFormikContext();
const handleSubmitBtnClick = () => {
submitForm();
};
const handleCancelBtnClick = () => {
closeDrawer(name);
};
return (
<Group spacing={10} className={styles.footerActions}>
<Button onClick={handleSubmitBtnClick} intent={Intent.PRIMARY}>
Save
</Button>
<Button onClick={handleCancelBtnClick}>Cancel</Button>
</Group>
);
}
const InvoiceCustomizeFooterActions = R.compose(withDrawerActions)(
InvoiceCustomizeFooterActionsRoot,
);

View File

@@ -1,16 +1,44 @@
import { Box, FFormGroup } from '@/components';
import { FColorField } from './FColorField';
import { Box, FFormGroup, FSwitch, Stack } from '@/components';
import { FColorInput } from './FColorField';
import styles from './InvoiceCustomizeFields.module.scss';
import { Classes } from '@blueprintjs/core';
export function InvoiceCustomizeGeneralField() {
return (
<Box>
<FFormGroup name={'primaryColor'} label={'Primary Color'} inline>
<FColorField name={'primaryColor'} />
<Stack style={{ padding: 20, flex: '1 1 auto' }}>
<Stack>
<h2>General Branding</h2>
<p className={Classes.TEXT_MUTED}>
Set your invoice details to be automatically applied every timeyou
create a new invoice.
</p>
</Stack>
<FFormGroup
name={'primaryColor'}
label={'Primary Color'}
inline
className={styles.fieldGroup}
>
<FColorInput name={'primaryColor'} />
</FFormGroup>
<FFormGroup name={'secondaryColor'} label={'Secondary Color'} inline>
<FColorField name={'secondaryColor'} />
<FFormGroup
name={'secondaryColor'}
label={'Secondary Color'}
inline
className={styles.fieldGroup}
>
<FColorInput name={'secondaryColor'} />
</FFormGroup>
</Box>
<FFormGroup name={'showLogo'} label={'Logo'}>
<FSwitch
name={'showLogo'}
label={'Display company logo in the paper'}
large
/>
</FFormGroup>
</Stack>
);
}

View File

@@ -9,6 +9,7 @@
padding: 5px 5px 5px 20px;
position: relative;
background-color: #fff;
z-index: 1;
}
.title{

View File

@@ -1,6 +1,6 @@
import { Group, Icon } from '@/components';
import styles from './InvoiceCustomizeHeader.module.scss';
import { Button, Classes } from '@blueprintjs/core';
import styles from './InvoiceCustomizeHeader.module.scss';
interface InvoiceCustomizeHeaderProps {
label?: string;

View File

@@ -1,12 +1,29 @@
// @ts-nocheck
import * as R from 'ramda';
import { Stack } from '@/components';
import { InvoiceCustomizeHeader } from './InvoiceCustomizeHeader';
import { InvoiceCustomizePreviewContent } from './InvoiceCustomizePreviewContent';
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
export function InvoiceCustomizePreview() {
function InvoiceCustomizePreviewRoot({ closeDrawer }) {
const { name } = useDrawerContext();
const handleCloseBtnClick = () => {
closeDrawer(name);
};
return (
<Stack spacing={0} style={{ borderLeft: '1px solid #D9D9D9' }}>
<InvoiceCustomizeHeader label={'Preview'} closeButton />
<Stack spacing={0} style={{ borderLeft: '1px solid #D9D9D9', height: '100vh' }}>
<InvoiceCustomizeHeader
label={'Preview'}
closeButton
onClose={handleCloseBtnClick}
/>
<InvoiceCustomizePreviewContent />
</Stack>
);
}
export const InvoiceCustomizePreview = R.compose(withDrawerActions)(
InvoiceCustomizePreviewRoot,
);

View File

@@ -3,7 +3,7 @@ import { PaperTemplate } from './PaperTemplate';
export function InvoiceCustomizePreviewContent() {
return (
<Box style={{ padding: 20, backgroundColor: '#F5F5F5' }}>
<Box style={{ padding: 20, backgroundColor: '#F5F5F5', overflow: 'auto' }}>
<PaperTemplate />
</Box>
);

View File

@@ -7,8 +7,15 @@
.content{
padding: 5px;
flex: 1;
border-right: 1px solid #E1E1E1;
}
.tabsList{
width: 100%;
flex: 1;
:global .bp4-tab-list{
flex: 1;
}
}

View File

@@ -1,4 +1,4 @@
import { Box } from '@/components';
import { Box, Stack } from '@/components';
import { Tab, Tabs } from '@blueprintjs/core';
import { InvoiceCustomizeHeader } from './InvoiceCustomizeHeader';
import styles from './InvoiceCustomizeTabs.module.scss';
@@ -14,16 +14,22 @@ export function InvoiceCustomizeTabs() {
setCurrentTabId(value);
};
return (
<Box className={styles.root}>
<Stack spacing={0} className={styles.root}>
<InvoiceCustomizeHeader label={''} />
<Box className={styles.content}>
<Tabs vertical fill onChange={handleChange} className={styles.tabsList}>
<Tabs
vertical
fill
large
onChange={handleChange}
className={styles.tabsList}
>
<Tab id="general" title="General" />
<Tab id="content" title="Content" />
<Tab id="total" title="Total" />
</Tabs>
</Box>
</Box>
</Stack>
);
}

View File

@@ -0,0 +1,37 @@
import { FInputGroup, FSwitch, Group, Stack } from '@/components';
import { Classes } from '@blueprintjs/core';
const items = [
{ key: 'dueAmount', label: 'Due Amount' },
{ key: 'billedTo', label: 'Billed To' },
{ key: 'balanceDue', label: 'Balance Due' },
{ key: 'termsConditions', label: 'Terms & Conditions' },
];
export function InvoiceCustomizeContentFields() {
return (
<Stack style={{ padding: 20, flex: '1 1 auto' }}>
<Stack>
<h2>General Branding</h2>
<p className={Classes.TEXT_MUTED}>
Set your invoice details to be automatically applied every timeyou
create a new invoice.
</p>
</Stack>
<h1>Header</h1>
<Stack>
{items.map((item, index) => (
<Group position={'apart'} key={index}>
<FSwitch name={`item.${item.key}.enabled`} label={item.label} />
<FInputGroup
name={'item.dueAmount.text'}
placeholder={item.label}
/>
</Group>
))}
</Stack>
</Stack>
);
}