feat: wip invoice payment email template

This commit is contained in:
Ahmed Bouhuolia
2024-10-27 15:09:08 +02:00
parent 42ee8ed9fa
commit 01cc0568f9
8 changed files with 1540 additions and 455 deletions

View File

@@ -37,9 +37,9 @@
"@types/lodash": "^4.14.172",
"@types/node": "^14.14.9",
"@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-dom": "^16.9.16",
"@types/react-helmet": "^6.1.11",
"@types/react-redux": "^7.1.24",
"@types/react-router-dom": "^5.3.3",

1552
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,6 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint .",
"preview": "vite preview",
@@ -23,11 +22,15 @@
"./dist/style.css": "./dist/style.css"
},
"dependencies": {
"@react-email/components": "0.0.25",
"tailwindcss": "^3.4.14",
"vite-plugin-dts": "^4.3.0",
"vitest": "^2.1.3"
"vitest": "^2.1.3",
"react-dom": "18.3.1",
"react": "18.3.1"
},
"devDependencies": {
"@eslint/js": "^9.13.0",
"@storybook/addon-essentials": "7.2.2",
"@storybook/addon-interactions": "7.2.2",
"@storybook/addon-links": "7.2.2",
@@ -36,22 +39,17 @@
"@storybook/react": "7.2.2",
"@storybook/react-vite": "7.2.2",
"@storybook/testing-library": "0.2.0",
"@eslint/js": "^9.13.0",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.1",
"@types/react": "18.3.4",
"@types/react-dom": "18.3.0",
"@vitejs/plugin-react": "^4.3.3",
"eslint": "^9.13.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.13",
"eslint-plugin-storybook": "0.6.13",
"globals": "^15.11.0",
"storybook": "7.2.2",
"typescript": "~5.6.2",
"typescript-eslint": "^8.10.0",
"storybook": "7.2.2",
"vite": "^5.4.9"
},
"peerDependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
}
}

View File

@@ -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' },
],
};

View 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',
};

View File

@@ -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,
}

View File

@@ -1,7 +0,0 @@
export interface TestProps {
a?: string;
}
export const Test = (props: TestProps) => {
return <h1>asdasd</h1>
}

View File

@@ -1,21 +1,25 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"composite": false,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"inlineSources": false,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
"moduleResolution": "node",
"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"],
"exclude": ["./src/**/*.stories.*"]
"include": ["."],
"exclude": ["dist", "build", "node_modules"]
}