mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 14:20:31 +00:00
feat: wip invoice payment email template
This commit is contained in:
@@ -37,9 +37,9 @@
|
|||||||
"@types/lodash": "^4.14.172",
|
"@types/lodash": "^4.14.172",
|
||||||
"@types/node": "^14.14.9",
|
"@types/node": "^14.14.9",
|
||||||
"@types/ramda": "^0.28.14",
|
"@types/ramda": "^0.28.14",
|
||||||
"@types/react": "^16.14.28",
|
"@types/react": "18.3.4",
|
||||||
|
"@types/react-dom": "18.3.0",
|
||||||
"@types/react-body-classname": "^1.1.7",
|
"@types/react-body-classname": "^1.1.7",
|
||||||
"@types/react-dom": "^16.9.16",
|
|
||||||
"@types/react-helmet": "^6.1.11",
|
"@types/react-helmet": "^6.1.11",
|
||||||
"@types/react-redux": "^7.1.24",
|
"@types/react-redux": "^7.1.24",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
|
|||||||
1552
pnpm-lock.yaml
generated
1552
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,6 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
@@ -23,11 +22,15 @@
|
|||||||
"./dist/style.css": "./dist/style.css"
|
"./dist/style.css": "./dist/style.css"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-email/components": "0.0.25",
|
||||||
"tailwindcss": "^3.4.14",
|
"tailwindcss": "^3.4.14",
|
||||||
"vite-plugin-dts": "^4.3.0",
|
"vite-plugin-dts": "^4.3.0",
|
||||||
"vitest": "^2.1.3"
|
"vitest": "^2.1.3",
|
||||||
|
"react-dom": "18.3.1",
|
||||||
|
"react": "18.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.13.0",
|
||||||
"@storybook/addon-essentials": "7.2.2",
|
"@storybook/addon-essentials": "7.2.2",
|
||||||
"@storybook/addon-interactions": "7.2.2",
|
"@storybook/addon-interactions": "7.2.2",
|
||||||
"@storybook/addon-links": "7.2.2",
|
"@storybook/addon-links": "7.2.2",
|
||||||
@@ -36,22 +39,17 @@
|
|||||||
"@storybook/react": "7.2.2",
|
"@storybook/react": "7.2.2",
|
||||||
"@storybook/react-vite": "7.2.2",
|
"@storybook/react-vite": "7.2.2",
|
||||||
"@storybook/testing-library": "0.2.0",
|
"@storybook/testing-library": "0.2.0",
|
||||||
"@eslint/js": "^9.13.0",
|
"@types/react": "18.3.4",
|
||||||
"@types/react": "^18.3.11",
|
"@types/react-dom": "18.3.0",
|
||||||
"@types/react-dom": "^18.3.1",
|
|
||||||
"@vitejs/plugin-react": "^4.3.3",
|
"@vitejs/plugin-react": "^4.3.3",
|
||||||
"eslint": "^9.13.0",
|
"eslint": "^9.13.0",
|
||||||
"eslint-plugin-react-hooks": "^5.0.0",
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.13",
|
"eslint-plugin-react-refresh": "^0.4.13",
|
||||||
"eslint-plugin-storybook": "0.6.13",
|
"eslint-plugin-storybook": "0.6.13",
|
||||||
"globals": "^15.11.0",
|
"globals": "^15.11.0",
|
||||||
|
"storybook": "7.2.2",
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.6.2",
|
||||||
"typescript-eslint": "^8.10.0",
|
"typescript-eslint": "^8.10.0",
|
||||||
"storybook": "7.2.2",
|
|
||||||
"vite": "^5.4.9"
|
"vite": "^5.4.9"
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^18.3.1",
|
|
||||||
"react-dom": "^18.3.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Meta } from '@storybook/react';
|
||||||
|
import { StoryFn } from '@storybook/react';
|
||||||
|
import {
|
||||||
|
InvoicePaymentEmail,
|
||||||
|
InvoicePaymentEmailProps,
|
||||||
|
} from './InvoicePaymentEmail';
|
||||||
|
|
||||||
|
const meta: Meta<typeof InvoicePaymentEmail> = {
|
||||||
|
title: 'Invoice Payment Email',
|
||||||
|
component: InvoicePaymentEmail,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
const Template: StoryFn<typeof InvoicePaymentEmail> = (
|
||||||
|
args: InvoicePaymentEmailProps
|
||||||
|
) => <InvoicePaymentEmail {...args} />;
|
||||||
|
|
||||||
|
export const PreviewInvoicePaymentMail = Template.bind({});
|
||||||
|
|
||||||
|
PreviewInvoicePaymentMail.args = {
|
||||||
|
preview: 'Preview text',
|
||||||
|
companyName: 'ABC Company',
|
||||||
|
companyLogoUri: 'https://example.com/logo.png',
|
||||||
|
invoiceAmount: '100.00',
|
||||||
|
dueDate: '2022-12-31',
|
||||||
|
invoiceMessage: 'Thank you for your purchase!',
|
||||||
|
invoiceNumber: 'INV-001',
|
||||||
|
dueAmount: '100.00',
|
||||||
|
total: '100.00',
|
||||||
|
viewInvoiceButtonUrl: 'https://example.com/invoice',
|
||||||
|
items: [
|
||||||
|
{ label: 'Item 1', quantity: '1', rate: '50.00' },
|
||||||
|
{ label: 'Item 2', quantity: '2', rate: '25.00' },
|
||||||
|
],
|
||||||
|
};
|
||||||
296
shared/email-components/src/lib/InvoicePaymentEmail.tsx
Normal file
296
shared/email-components/src/lib/InvoicePaymentEmail.tsx
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
import {
|
||||||
|
Html,
|
||||||
|
Button,
|
||||||
|
Head,
|
||||||
|
Body,
|
||||||
|
Container,
|
||||||
|
Section,
|
||||||
|
Heading,
|
||||||
|
Text,
|
||||||
|
Preview,
|
||||||
|
Tailwind,
|
||||||
|
Row,
|
||||||
|
Column,
|
||||||
|
render,
|
||||||
|
} from '@react-email/components';
|
||||||
|
import { CSSProperties } from 'react';
|
||||||
|
|
||||||
|
export interface InvoicePaymentEmailProps {
|
||||||
|
preview: string;
|
||||||
|
|
||||||
|
// # Company
|
||||||
|
companyName?: string;
|
||||||
|
companyLogoUri?: string;
|
||||||
|
|
||||||
|
// # Invoice amount
|
||||||
|
invoiceAmount: string;
|
||||||
|
|
||||||
|
// # Invoice message
|
||||||
|
invoiceMessage: string;
|
||||||
|
|
||||||
|
// # Invoice total
|
||||||
|
total: string;
|
||||||
|
totalLabel?: string;
|
||||||
|
|
||||||
|
// # Invoice due amount
|
||||||
|
dueAmount: string;
|
||||||
|
dueAmountLabel?: string;
|
||||||
|
|
||||||
|
// # Due date
|
||||||
|
dueDate: string;
|
||||||
|
dueDateLabel?: string;
|
||||||
|
|
||||||
|
// # Invoice number
|
||||||
|
invoiceNumberLabel?: string;
|
||||||
|
invoiceNumber: string;
|
||||||
|
|
||||||
|
// # View invoice button
|
||||||
|
viewInvoiceButtonLabel?: string;
|
||||||
|
viewInvoiceButtonUrl: string;
|
||||||
|
|
||||||
|
// # Items
|
||||||
|
items: Array<{ label: string; quantity: string; rate: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InvoicePaymentEmail: React.FC<
|
||||||
|
Readonly<InvoicePaymentEmailProps>
|
||||||
|
> = ({
|
||||||
|
preview,
|
||||||
|
|
||||||
|
// # Company
|
||||||
|
companyName,
|
||||||
|
companyLogoUri,
|
||||||
|
|
||||||
|
// # Invoice amount
|
||||||
|
invoiceAmount,
|
||||||
|
|
||||||
|
// # Invoice message
|
||||||
|
invoiceMessage,
|
||||||
|
|
||||||
|
// # Due date
|
||||||
|
dueDate,
|
||||||
|
dueDateLabel = 'Due {dueDate}',
|
||||||
|
|
||||||
|
// # Invoice number
|
||||||
|
invoiceNumber,
|
||||||
|
invoiceNumberLabel = 'Invoice # {invoiceNumber}',
|
||||||
|
|
||||||
|
// # invoice total
|
||||||
|
total,
|
||||||
|
totalLabel = 'Total',
|
||||||
|
|
||||||
|
// # Invoice due amount
|
||||||
|
dueAmountLabel = 'Due Amount',
|
||||||
|
dueAmount,
|
||||||
|
|
||||||
|
// # View invoice button
|
||||||
|
viewInvoiceButtonLabel = 'View Invoice',
|
||||||
|
viewInvoiceButtonUrl,
|
||||||
|
|
||||||
|
items,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Html lang="en">
|
||||||
|
<Head />
|
||||||
|
<Preview>{preview}</Preview>
|
||||||
|
|
||||||
|
<Tailwind>
|
||||||
|
<Body style={bodyStyle}>
|
||||||
|
<Container style={containerStyle}>
|
||||||
|
<Section style={mainSectionStyle}>
|
||||||
|
<Section style={headerInfoStyle}>
|
||||||
|
<Row>
|
||||||
|
<Heading style={invoiceCompanyNameStyle}>
|
||||||
|
{companyName}
|
||||||
|
</Heading>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Text style={invoiceAmountStyle}>{invoiceAmount}</Text>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Text style={invoiceNumberStyle}>
|
||||||
|
{invoiceNumberLabel?.replace(
|
||||||
|
'{invoiceNumber}',
|
||||||
|
invoiceNumber
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Text style={invoiceDateStyle}>
|
||||||
|
{dueDateLabel.replace('{dueDate}', dueDate)}
|
||||||
|
</Text>
|
||||||
|
</Row>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Text style={invoiceMessageStyle}>{invoiceMessage}</Text>
|
||||||
|
<Button
|
||||||
|
href={viewInvoiceButtonUrl}
|
||||||
|
style={viewInvoiceButtonStyle}
|
||||||
|
>
|
||||||
|
{viewInvoiceButtonLabel}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Section style={totalsSectionStyle}>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<Row key={index} style={itemLineRowStyle}>
|
||||||
|
<Column width={'50%'}>
|
||||||
|
<Text style={listItemLabelStyle}>{item.label}</Text>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column width={'50%'}>
|
||||||
|
<Text style={listItemAmountStyle}>
|
||||||
|
{item.quantity} x {item.rate}
|
||||||
|
</Text>
|
||||||
|
</Column>
|
||||||
|
</Row>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Row style={dueAmounLineRowStyle}>
|
||||||
|
<Column width={'50%'}>
|
||||||
|
<Text style={dueAmountLineItemLabelStyle}>
|
||||||
|
{dueAmountLabel}
|
||||||
|
</Text>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column width={'50%'}>
|
||||||
|
<Text style={dueAmountLineItemAmountStyle}>
|
||||||
|
{dueAmount}
|
||||||
|
</Text>
|
||||||
|
</Column>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row style={totalLineRowStyle}>
|
||||||
|
<Column width={'50%'}>
|
||||||
|
<Text style={totalLineItemLabelStyle}>{totalLabel}</Text>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column width={'50%'}>
|
||||||
|
<Text style={totalLineItemAmountStyle}>{total}</Text>
|
||||||
|
</Column>
|
||||||
|
</Row>
|
||||||
|
</Section>
|
||||||
|
</Section>
|
||||||
|
</Container>
|
||||||
|
</Body>
|
||||||
|
</Tailwind>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const renderInvoicePaymentEmail = (props: InvoicePaymentEmailProps) => {
|
||||||
|
return render(<InvoicePaymentEmail {...props} />);
|
||||||
|
};
|
||||||
|
|
||||||
|
const bodyStyle: CSSProperties = {
|
||||||
|
backgroundColor: '#F5F5F5',
|
||||||
|
fontFamily:
|
||||||
|
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
|
||||||
|
|
||||||
|
padding: '40px 0',
|
||||||
|
};
|
||||||
|
|
||||||
|
const containerStyle: CSSProperties = {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: '500px',
|
||||||
|
padding: '35px 25px',
|
||||||
|
color: '#000',
|
||||||
|
borderRadius: '5px',
|
||||||
|
};
|
||||||
|
|
||||||
|
const headerInfoStyle: CSSProperties = {
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: 20,
|
||||||
|
};
|
||||||
|
const mainSectionStyle: CSSProperties = {};
|
||||||
|
|
||||||
|
const invoiceAmountStyle: CSSProperties = {
|
||||||
|
margin: 0,
|
||||||
|
color: '#383E47',
|
||||||
|
fontWeight: 500,
|
||||||
|
};
|
||||||
|
const invoiceNumberStyle: CSSProperties = {
|
||||||
|
margin: 0,
|
||||||
|
fontSize: '13px',
|
||||||
|
color: '#404854',
|
||||||
|
};
|
||||||
|
const invoiceDateStyle: CSSProperties = {
|
||||||
|
margin: 0,
|
||||||
|
fontSize: '13px',
|
||||||
|
color: '#404854',
|
||||||
|
};
|
||||||
|
|
||||||
|
const invoiceCompanyNameStyle: CSSProperties = {
|
||||||
|
margin: 0,
|
||||||
|
fontSize: '18px',
|
||||||
|
fontWeight: 500,
|
||||||
|
color: '#404854',
|
||||||
|
};
|
||||||
|
|
||||||
|
const viewInvoiceButtonStyle: CSSProperties = {
|
||||||
|
display: 'block',
|
||||||
|
cursor: 'pointer',
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: 16,
|
||||||
|
padding: '0 15px',
|
||||||
|
height: '38px',
|
||||||
|
lineHeight: '38px',
|
||||||
|
backgroundColor: 'rgb(0, 82, 204)',
|
||||||
|
color: '#fff',
|
||||||
|
borderRadius: '5px',
|
||||||
|
};
|
||||||
|
|
||||||
|
const listItemLabelStyle: CSSProperties = {
|
||||||
|
margin: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const listItemAmountStyle: CSSProperties = {
|
||||||
|
margin: 0,
|
||||||
|
textAlign: 'right',
|
||||||
|
};
|
||||||
|
|
||||||
|
const invoiceMessageStyle: CSSProperties = {
|
||||||
|
whiteSpace: 'pre-line',
|
||||||
|
color: '#252A31',
|
||||||
|
margin: '0 0 20px 0',
|
||||||
|
};
|
||||||
|
|
||||||
|
const dueAmounLineRowStyle: CSSProperties = {
|
||||||
|
borderBottom: '1px solid #000',
|
||||||
|
height: 40,
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalLineRowStyle: CSSProperties = {
|
||||||
|
borderBottom: '1px solid #000',
|
||||||
|
height: 40,
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalLineItemLabelStyle: CSSProperties = {
|
||||||
|
...listItemLabelStyle,
|
||||||
|
fontWeight: 500,
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalLineItemAmountStyle: CSSProperties = {
|
||||||
|
...listItemAmountStyle,
|
||||||
|
fontWeight: 600,
|
||||||
|
};
|
||||||
|
|
||||||
|
const dueAmountLineItemLabelStyle: CSSProperties = {
|
||||||
|
...listItemLabelStyle,
|
||||||
|
fontWeight: 500,
|
||||||
|
};
|
||||||
|
|
||||||
|
const dueAmountLineItemAmountStyle: CSSProperties = {
|
||||||
|
...listItemAmountStyle,
|
||||||
|
fontWeight: 600,
|
||||||
|
};
|
||||||
|
|
||||||
|
const itemLineRowStyle: CSSProperties = {
|
||||||
|
borderBottom: '1px solid #D9D9D9',
|
||||||
|
height: 40,
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalsSectionStyle = {
|
||||||
|
marginTop: '20px',
|
||||||
|
borderTop: '1px solid #D9D9D9',
|
||||||
|
};
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
// import { AtButton, AtButtonProps, AT_BUTTON_VARIANT } from '.'
|
|
||||||
// import { objectValuesToControls } from '../../../storybook-utils'
|
|
||||||
import { Meta } from '@storybook/react'
|
|
||||||
import { StoryFn } from '@storybook/react'
|
|
||||||
import { Test, TestProps } from './Test';
|
|
||||||
|
|
||||||
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
|
|
||||||
const meta: Meta<typeof Test> = {
|
|
||||||
title: 'Atoms/Button',
|
|
||||||
component: Test,
|
|
||||||
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
|
|
||||||
argTypes: {
|
|
||||||
// label: { control: 'text' },
|
|
||||||
// variant: objectValuesToControls(AT_BUTTON_VARIANT),
|
|
||||||
// onClick: { action: 'clicked' },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
export default meta
|
|
||||||
|
|
||||||
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
|
|
||||||
const Template: StoryFn<typeof Test> = (args: TestProps) => <Test {...args} />
|
|
||||||
|
|
||||||
export const Primary = Template.bind({})
|
|
||||||
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
|
||||||
Primary.args = {
|
|
||||||
label: 'Button',
|
|
||||||
variant: 'PRIMARY',
|
|
||||||
onClick: () => alert('clicking primary'),
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Secondary = Template.bind({})
|
|
||||||
Secondary.args = {
|
|
||||||
label: 'Button',
|
|
||||||
variant: 'SECONDARY',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Tertiary = Template.bind({})
|
|
||||||
Tertiary.args = {
|
|
||||||
label: 'Button',
|
|
||||||
variant: 'TERTIARY',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Disabled = Template.bind({})
|
|
||||||
Disabled.args = {
|
|
||||||
label: 'Button',
|
|
||||||
isDisabled: true,
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
|
|
||||||
export interface TestProps {
|
|
||||||
a?: string;
|
|
||||||
}
|
|
||||||
export const Test = (props: TestProps) => {
|
|
||||||
return <h1>asdasd</h1>
|
|
||||||
}
|
|
||||||
@@ -1,21 +1,25 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"composite": false,
|
||||||
"useDefineForClassFields": true,
|
"declaration": true,
|
||||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
"declarationMap": true,
|
||||||
"allowJs": false,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
|
||||||
"esModuleInterop": false,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"module": "ESNext",
|
"inlineSources": false,
|
||||||
"moduleResolution": "Node",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"moduleResolution": "node",
|
||||||
"jsx": "react-jsx"
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"preserveWatchOutput": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"target": "ESNext",
|
||||||
|
"types": ["vitest/globals"]
|
||||||
},
|
},
|
||||||
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
|
"include": ["."],
|
||||||
"exclude": ["./src/**/*.stories.*"]
|
"exclude": ["dist", "build", "node_modules"]
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user