Initial commit: backend, storefront, vendor-panel added

This commit is contained in:
2025-08-01 11:05:32 +08:00
commit 08174125d2
2958 changed files with 310810 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
{
"name": "@mercurjs/resend",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"!dist/**/__tests__",
"!dist/**/__mocks__",
"!dist/**/__fixtures__"
],
"engines": {
"node": ">=20"
},
"license": "MIT",
"scripts": {
"build": "rimraf dist && tsc --build",
"migration:initial": " MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:create --initial",
"migration:create": " MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:create",
"migration:up": " MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:up",
"orm:cache:clear": " MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm cache:clear"
},
"devDependencies": {
"@medusajs/framework": "2.8.6",
"@medusajs/test-utils": "2.8.6",
"@mercurjs/framework": "*",
"@mikro-orm/cli": "6.4.3",
"@mikro-orm/core": "6.4.3",
"@mikro-orm/migrations": "6.4.3",
"@mikro-orm/postgresql": "6.4.3",
"@swc/core": "^1.7.28",
"@swc/jest": "^0.2.36",
"jest": "^29.7.0",
"rimraf": "^3.0.2",
"tsc-alias": "^1.8.6",
"typescript": "^5.6.2"
},
"peerDependencies": {
"@medusajs/framework": "2.8.6",
"@mikro-orm/core": "6.4.3",
"@mikro-orm/migrations": "6.4.3",
"@mikro-orm/postgresql": "6.4.3",
"awilix": "^8.0.1"
}
}

View File

@@ -0,0 +1,32 @@
interface EmailTemplateProps {
data: {
request_type: string
seller_name: string
}
}
export const AdminRequestCreatedEmailTemplate: React.FC<Readonly<EmailTemplateProps>> = ({ data }) => {
return (
<div style={{
maxWidth: 600,
margin: '0 auto',
fontFamily: 'Arial, sans-serif',
color: '#222',
background: '#fff',
padding: 24,
borderRadius: 10
}}>
<h1 style={{ fontSize: '2rem', marginBottom: 8 }}>
Hello, <span role="img" aria-label="wave">👋</span>
</h1>
<p style={{ fontSize: '1.1rem', marginBottom: 16 }}>
{data.seller_name} has requested to create a new {data.request_type}. Please review the request and approve it in admin panel.
</p>
<div style={{ marginTop: 32 }}>
<div>Best regards,</div>
<div style={{ fontWeight: 600 }}>The Mercur Team</div>
<div style={{ color: '#888', marginTop: 4 }}>mercur.js</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,38 @@
interface EmailTemplateProps {
data: {
request_address: string,
seller_name: string
}
}
export const AdminSellerRequestCreatedEmailTemplate: React.FC<Readonly<EmailTemplateProps>> = ({ data }) => {
return (
<div style={{
maxWidth: 600,
margin: '0 auto',
fontFamily: 'Arial, sans-serif',
color: '#222',
background: '#fff',
padding: 24,
borderRadius: 10
}}>
<h1 style={{ fontSize: '2rem', marginBottom: 8 }}>
Hello, <span role="img" aria-label="wave">👋</span>
</h1>
<p style={{ fontSize: '1.1rem', marginBottom: 16 }}>
{data.seller_name} has requested to join the platform. Please review the request and approve it in admin panel.
</p>
<div>
<a href={data.request_address}>
<button>Review Request</button>
</a>
</div>
<div style={{ marginTop: 32 }}>
<div>Best regards,</div>
<div style={{ fontWeight: 600 }}>The Mercur Team</div>
<div style={{ color: '#888', marginTop: 4 }}>mercur.js</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,53 @@
interface EmailTemplateProps {
data: {
user_name: string;
};
}
export const BuyerAccountCreatedEmailTemplate: React.FC<
Readonly<EmailTemplateProps>
> = ({ data }) => {
return (
<div
style={{
maxWidth: "480px",
margin: "40px auto",
padding: "32px 24px",
borderRadius: "12px",
background: "#fff",
boxShadow: "0 2px 12px rgba(0,0,0,0.07)",
fontFamily: "Arial, sans-serif",
color: "#222",
}}
>
<h1 style={{ fontSize: "2rem", marginBottom: "16px", color: "#222" }}>
Welcome to Mercur, {data.user_name}!
</h1>
<p style={{ fontSize: "1.1rem", marginBottom: "24px" }}>
Were excited to have you join us on this journey.
<br />
Your account has been created successfully.
</p>
<a
href="https://mercurjs.com"
style={{
display: "inline-block",
padding: "12px 28px",
background: "#222",
color: "#fff",
borderRadius: "6px",
textDecoration: "none",
fontWeight: 600,
marginBottom: "32px",
}}
>
Visit Mercur
</a>
<div style={{ marginTop: 32 }}>
<div>Best regards,</div>
<div style={{ fontWeight: 600 }}>The Mercur Team</div>
<div style={{ color: "#888", marginTop: 4 }}>mercurjs.com</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,121 @@
interface EmailTemplateProps {
data: {
order_address: string,
order: {
id: string,
display_id?: string,
items: any[]
item?: any
}
}
}
export const BuyerCancelOrderEmailTemplate: React.FC<Readonly<EmailTemplateProps>> = ({ data }) => {
const { order } = data;
const itemsArray = order.items?.flat() || [order.item].flat();
return (
<div style={{
maxWidth: 600,
margin: '0 auto',
fontFamily: 'Arial, sans-serif',
color: '#222',
background: '#fff',
padding: 24,
borderRadius: 10
}}>
<h1 style={{ fontSize: '2rem', marginBottom: 8 }}>Your order has been canceled</h1>
<p style={{ fontSize: '1.1rem', marginBottom: 24 }}>
We're sorry, but your order <b>#{order.display_id || order.id}</b> has been canceled by the seller.
</p>
<h3 style={{ marginTop: 32, marginBottom: 12 }}>Order items:</h3>
<table style={{ width: '100%', borderCollapse: 'collapse', marginBottom: 32 }}>
<thead>
<tr>
<th style={{ textAlign: 'left', padding: '8px', borderBottom: '1px solid #eee' }}>Product</th>
<th style={{ textAlign: 'right', padding: '8px', borderBottom: '1px solid #eee' }}>Amount</th>
<th style={{ textAlign: 'right', padding: '8px', borderBottom: '1px solid #eee' }}>Qty</th>
<th style={{ textAlign: 'right', padding: '8px', borderBottom: '1px solid #eee' }}>Total</th>
</tr>
</thead>
<tbody>
{itemsArray.map((item: any, idx: number) => (
<tr key={item.id || idx} style={{ borderBottom: '1px solid #f3f3f3' }}>
<td style={{ padding: '12px 8px', verticalAlign: 'top' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
{item.thumbnail && (
<img
src={item.thumbnail}
alt={item.product_title}
style={{
width: 48,
height: 48,
objectFit: 'cover',
borderRadius: 6,
marginRight: 12,
border: '1px solid #eee'
}}
/>
)}
<div>
<div style={{ fontWeight: 600 }}>{item.product_title}</div>
<div style={{ fontSize: 13, color: '#555' }}>
Variant: {item.variant_title}
</div>
</div>
</div>
</td>
<td style={{ textAlign: 'right', padding: '12px 8px', verticalAlign: 'top' }}>
{item.unit_price} eur
</td>
<td style={{ textAlign: 'right', padding: '12px 8px', verticalAlign: 'top' }}>
{item.quantity}
</td>
<td style={{ textAlign: 'right', padding: '12px 8px', verticalAlign: 'top' }}>
{item.unit_price * item.quantity} eur
</td>
</tr>
))}
</tbody>
</table>
<div style={{ margin: '32px 0 16px 0', fontSize: '1rem' }}>
If you have any questions, please contact <b>Mercur Support</b>.
</div>
<div style={{ marginBottom: 24 }}>
<a
href={data.order_address}
style={{
display: 'inline-block',
padding: '10px 24px',
background: '#222',
color: '#fff',
borderRadius: 6,
textDecoration: 'none',
fontWeight: 600,
marginBottom: 8
}}
>
View Order Details
</a>
<div style={{ fontSize: 13, color: '#555', marginTop: 8 }}>
If you cant click the button, heres your link: <br />
<span style={{ color: '#0070f3' }}>{data.order_address}</span>
</div>
</div>
<div style={{ fontSize: 13, color: '#888', marginBottom: 24 }}>
You received this email because you made a purchase or sale on the Mercur marketplace.
If you have any questions, please contact our support team.
</div>
<div style={{ marginTop: 32 }}>
<div>Best regards,</div>
<div style={{ fontWeight: 600 }}>The Mercur Team</div>
<div style={{ color: '#888', marginTop: 4 }}>mercur.js</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,163 @@
interface EmailTemplateProps {
data: {
user_name: string,
order_address: string,
order_id: string,
order: {
display_id: string,
items: any[],
currency_code: string,
shipping_methods: {
amount: number,
name: string
}[],
total: number,
email: string,
shipping_address: {
first_name: string,
last_name: string,
company: string,
address_1: string,
address_2: string,
city: string,
province: string,
postal_code: string,
phone: string
}
}
}
}
export const BuyerNewOrderEmailTemplate: React.FC<Readonly<EmailTemplateProps>> = ({ data }) => {
const { order } = data;
return (
<div style={{
maxWidth: 600,
margin: '0 auto',
fontFamily: 'Arial, sans-serif',
color: '#222',
background: '#fff',
padding: 24,
borderRadius: 10
}}>
<h1 style={{ fontSize: '2rem', marginBottom: 8 }}>
Thank you for your order, {data.user_name}!<br />
Your order #{order.display_id} has been placed!
</h1>
<p style={{ fontSize: '1.1rem', marginBottom: 24 }}>
Thank you for placing order #{order.display_id}.<br />
</p>
<div style={{ marginBottom: 24 }}>
<a
href={data.order_address}
style={{
display: 'inline-block',
padding: '10px 24px',
background: '#222',
color: '#fff',
borderRadius: 6,
textDecoration: 'none',
fontWeight: 600,
marginBottom: 8
}}
>
Order details
</a>
<div style={{ fontSize: 13, color: '#555', marginTop: 8 }}>
If you cant click the button, heres your link: <br />
<span style={{ color: '#0070f3' }}>{data.order_address}</span>
</div>
</div>
<h3 style={{ marginTop: 32, marginBottom: 12 }}>Heres the breakdown:</h3>
<table style={{ width: '100%', borderCollapse: 'collapse', marginBottom: 32 }}>
<thead>
<tr>
<th style={{ textAlign: 'left', padding: '8px', borderBottom: '1px solid #eee' }}>Product</th>
<th style={{ textAlign: 'right', padding: '8px', borderBottom: '1px solid #eee' }}>Amount</th>
<th style={{ textAlign: 'right', padding: '8px', borderBottom: '1px solid #eee' }}>Qty</th>
<th style={{ textAlign: 'right', padding: '8px', borderBottom: '1px solid #eee' }}>Total</th>
</tr>
</thead>
<tbody>
{order.items.map((item: any, index: number) => (
<tr key={index} style={{ borderBottom: '1px solid #f3f3f3' }}>
<td style={{ padding: '12px 8px', verticalAlign: 'top' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<img
src={item.thumbnail}
alt={`Thumbnail of ${item.product_title}`}
style={{
width: '40px',
height: '40px',
objectFit: 'cover',
marginRight: '10px',
borderRadius: '4px',
border: '1px solid #eee'
}}
/>
<div>
<div style={{ fontWeight: 600 }}>{item.product_title}</div>
<div style={{ fontSize: '12px', color: '#555' }}>
Variant: {item.variant_title}
</div>
</div>
</div>
</td>
<td style={{ textAlign: 'right', padding: '12px 8px', verticalAlign: 'top' }}>
{item.unit_price} {order.currency_code}
</td>
<td style={{ textAlign: 'right', padding: '12px 8px', verticalAlign: 'top' }}>{item.quantity}</td>
<td style={{ textAlign: 'right', padding: '12px 8px', verticalAlign: 'top' }}>
{item.unit_price * item.quantity} {order.currency_code}
</td>
</tr>
))}
</tbody>
<tfoot>
<tr>
<td><b>Delivery:</b></td>
<td colSpan={3}>
{order.shipping_methods[0].amount} {order.currency_code}
</td>
</tr>
<tr>
<td><b>Total:</b></td>
<td colSpan={3}>
{order.total} {order.currency_code}
</td>
</tr>
</tfoot>
</table>
<div style={{ marginBottom: 24 }}>
<div>
<p style={{ marginBottom: 4 }}>
<strong>Shipping address:</strong><br />
{order.shipping_address.first_name} {order.shipping_address.last_name},<br />
{order.shipping_address?.company ? `${order.shipping_address.company}, ` : ''}
{order.shipping_address.address_1}
{order.shipping_address.address_2 && `, ${order.shipping_address.address_2}`}, {order.shipping_address.postal_code} {order.shipping_address.city}
{order.shipping_address.province ? `, ${order.shipping_address.province}` : ''}
<br />
{order.email}, {order.shipping_address.phone}
</p>
</div>
<div>
<p>
<strong>Delivery method:</strong><br />
{order.shipping_methods[0].name}
</p>
</div>
</div>
<div style={{ fontSize: 13, color: '#888', marginBottom: 24 }}>
You received this email because you made a purchase or sale on the Mercur marketplace.<br />
If you have any questions, please contact our support team.
</div>
<div style={{ marginTop: 32 }}>
<div>Best regards,</div>
<div style={{ fontWeight: 600 }}>The Mercur Team</div>
<div style={{ color: '#888', marginTop: 4 }}>mercurjs.com</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,71 @@
interface EmailTemplateProps {
data: {
user_name: string,
host: string,
order_id: string,
order: {
id: string,
display_id: string,
trackingNumber: string,
items: any[],
currency_code: string,
item_total: number,
shipping_methods: {
amount: number,
name: string
}[],
total: number
email: string
shipping_address: {
first_name: string,
last_name: string,
company: string,
address_1: string,
address_2: string,
city: string,
province: string,
postal_code: string,
phone: string
}
}
}
}
export const BuyerOrderShippedEmailTemplate: React.FC<Readonly<EmailTemplateProps>> = ({ data }) => {
return (
<div>
<h1>Your order #{data.order.trackingNumber} has been shipped!</h1>
<p>The package is on its way and will be in your hands soon.</p>
<div>
<p>
<strong>Shipping address:</strong>
</p>
<p>
{data.order.shipping_address.first_name} {data.order.shipping_address.last_name}
,<br />
{data.order.shipping_address?.company ? `${data.order.shipping_address.company}, ` : ''}
{data.order.shipping_address.address_1}
{data.order.shipping_address.address_2}, {data.order.shipping_address.postal_code}{' '}
{data.order.shipping_address.city}
{data.order.shipping_address.province ? `, ${data.order.shipping_address.province}` : ''}
<br />
{data.order.email}, {data.order.shipping_address.phone}
</p>
</div>
<p>
<a href={`${data.host}/orders/${data.order.id}`}>View Order Details</a>
If you cant click the button, no worries! Heres your link: {`${data.host}/orders/${data.order.id}`}
</p>
<p>
You received this email because you made a purchase or sale on the Mercur marketplace. If you have any
questions, please contact our support team.
</p>
<div style={{ marginTop: 32 }}>
<div>Best regards,</div>
<div style={{ fontWeight: 600 }}>The Mercur Team</div>
<div style={{ color: '#888', marginTop: 4 }}>mercurjs.com</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,36 @@
interface EmailTemplateProps {
data: {
url: string,
}
}
export const ForgotPasswordEmailTemplate: React.FC<Readonly<EmailTemplateProps>> = ({ data }) => {
return (
<div style={{
maxWidth: 600,
margin: '0 auto',
fontFamily: 'Arial, sans-serif',
color: '#222',
background: '#fff',
padding: 24,
borderRadius: 10
}}>
<h1>Have you forgotten your password?</h1>
<p>
We have received a request to reset the password for your Mercur account. Please click the button below to set a
new password. Please note, the link is valid for the next 24 hours only.
</p>
<div>
<a href={`${data.url}`}>
<button>Reset Password</button>
</a>
</div>
<p>If you did not request this change, please ignore this email.</p>
<div style={{ marginTop: 32 }}>
<div>Best regards,</div>
<div style={{ fontWeight: 600 }}>The Mercur Team</div>
<div style={{ color: '#888', marginTop: 4 }}>mercurjs.com</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,42 @@
import { AdminRequestCreatedEmailTemplate } from "./admin-request-created";
import { AdminSellerRequestCreatedEmailTemplate } from "./admin-seller-request-created";
import { BuyerAccountCreatedEmailTemplate } from "./buyer-account-created";
import { BuyerCancelOrderEmailTemplate } from "./buyer-cancel-order";
import { BuyerNewOrderEmailTemplate } from "./buyer-new-order";
import { BuyerOrderShippedEmailTemplate } from "./buyer-shipped-order";
import { ForgotPasswordEmailTemplate } from "./forgot-password";
import { NewSellerInviteEmailTemplate } from "./new-seller-invitation";
import { SellerAccountApprovedEmailTemplate } from "./seller-account-approved";
import { SellerAccountRejectedEmailTemplate } from "./seller-account-rejected";
import { SellerAccountSubmissionEmailTemplate } from "./seller-account-updates-submission";
import { SellerCanceledOrderEmailTemplate } from "./seller-canceled-order";
import { SellerNewOrderEmailTemplate } from "./seller-new-order";
import { SellerPayoutSummaryEmailTemplate } from "./seller-payout-summary";
import { SellerProductApprovedEmailTemplate } from "./seller-product-approved";
import { SellerProductRejectedEmailTemplate } from "./seller-product-rejected";
import { SellerOrderShippingEmailTemplate } from "./seller-shipping-order";
import { SellerTeamInviteEmailTemplate } from "./seller-team-invite";
import { SellerEmailVerifyEmailTemplate } from "./seller-verify-email";
export const emailTemplates: any = {
buyerAccountCreatedEmailTemplate: BuyerAccountCreatedEmailTemplate,
buyerCancelOrderEmailTemplate: BuyerCancelOrderEmailTemplate,
buyerNewOrderEmailTemplate: BuyerNewOrderEmailTemplate,
buyerOrderShippedEmailTemplate: BuyerOrderShippedEmailTemplate,
forgotPasswordEmailTemplate: ForgotPasswordEmailTemplate,
sellerAccountApprovedEmailTemplate: SellerAccountApprovedEmailTemplate,
sellerAccountRejectedEmailTemplate: SellerAccountRejectedEmailTemplate,
sellerAccountSubmissionEmailTemplate: SellerAccountSubmissionEmailTemplate,
sellerCanceledOrderEmailTemplate: SellerCanceledOrderEmailTemplate,
sellerNewOrderEmailTemplate: SellerNewOrderEmailTemplate,
sellerOrderShippingEmailTemplate: SellerOrderShippingEmailTemplate,
sellerTeamInviteEmailTemplate: SellerTeamInviteEmailTemplate,
sellerVerifyEmailTemplate: SellerEmailVerifyEmailTemplate,
newSellerInvitation: NewSellerInviteEmailTemplate,
sellerProductRejectedEmailTemplate: SellerProductRejectedEmailTemplate,
sellerProductApprovedEmailTemplate: SellerProductApprovedEmailTemplate,
adminRequestCreatedEmailTemplate: AdminRequestCreatedEmailTemplate,
adminSellerRequestCreatedEmailTemplate:
AdminSellerRequestCreatedEmailTemplate,
sellerPayoutSummaryEmailTemplate: SellerPayoutSummaryEmailTemplate,
};

View File

@@ -0,0 +1,56 @@
interface EmailTemplateProps {
data: {
url: string
}
}
export const NewSellerInviteEmailTemplate: React.FC<Readonly<EmailTemplateProps>> = ({ data }) => {
return (
<div style={{
maxWidth: 600,
margin: '0 auto',
fontFamily: 'Arial, sans-serif',
color: '#222',
background: '#fff',
padding: 24,
borderRadius: 10
}}>
<h1 style={{ fontSize: '2rem', marginBottom: '8px' }}>
You are invited to sell on MercurJS!
</h1>
<p style={{ fontSize: '1.1rem', marginBottom: '16px' }}>
To join the platform, please accept the invitation.<br />
</p>
<div style={{ marginBottom: 24 }}>
<a
href={`${data.url}`}
style={{
display: 'inline-block',
padding: '10px 24px',
background: '#222',
color: '#fff',
borderRadius: 6,
textDecoration: 'none',
fontWeight: 600,
marginBottom: 8
}}
>
Accept Invitation
</a>
<div style={{ fontSize: 13, color: '#555', marginTop: 8 }}>
If you cant click the button, heres your link: <br />
<span style={{ color: '#0070f3' }}>{`${data.url}`}</span>
</div>
</div>
<div style={{ fontSize: 13, color: '#888', marginBottom: 24 }}>
You received this email because you were invited to join the Mercur marketplace.<br />
If you have any questions, please contact our support team.
</div>
<div style={{ marginTop: 32 }}>
<div>Best regards,</div>
<div style={{ fontWeight: 600 }}>The Mercur Team</div>
<div style={{ color: '#888', marginTop: 4 }}>mercur.js</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,41 @@
interface EmailTemplateProps {
data: {
user_name: string
}
}
export const SellerAccountApprovedEmailTemplate: React.FC<Readonly<EmailTemplateProps>> = ({ data }) => {
return (
<div style={{
maxWidth: 600,
margin: '0 auto',
fontFamily: 'Arial, sans-serif',
color: '#222',
background: '#fff',
padding: 24,
borderRadius: 10
}}>
<h1 style={{ fontSize: '2rem', marginBottom: 8 }}>
Hello, {data.user_name} <span role="img" aria-label="wave">👋</span>
<br />
<span style={{ fontSize: '1.5rem', fontWeight: 600 }}>Your account has been approved!</span>
</h1>
<p style={{ fontSize: '1.1rem', marginBottom: 16 }}>
Were happy to let you know that your application has been approved! This means your account is now activated on
the Mercur marketplace.
</p>
<p style={{ fontSize: '1.1rem', marginBottom: 24 }}>
Thank you for choosing us. We are excited about your success and will support you every step of the way.
</p>
<div style={{ fontSize: 13, color: '#888', marginBottom: 24 }}>
You received this email because you registered as a seller on the Mercur marketplace.<br />
If you have any questions, please contact our support team.
</div>
<div style={{ marginTop: 32 }}>
<div>Best regards,</div>
<div style={{ fontWeight: 600 }}>The Mercur Team</div>
<div style={{ color: '#888', marginTop: 4 }}>mercur.js</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,35 @@
export const SellerAccountRejectedEmailTemplate: React.FC = () => {
return (
<div style={{
maxWidth: 600,
margin: '0 auto',
fontFamily: 'Arial, sans-serif',
color: '#222',
background: '#fff',
padding: 24,
borderRadius: 10
}}>
<h1 style={{ fontSize: '2rem', marginBottom: 8 }}>
We regret to inform you that your account application has been rejected.
</h1>
<p style={{ fontSize: '1.1rem', marginBottom: 16 }}>
We appreciate the effort you put into your application and thank you for considering our platform.
Unfortunately, after a careful review, we have determined that your application does not meet our current
requirements.
</p>
<p style={{ fontSize: '1.1rem', marginBottom: 24 }}>
If you have any questions or need further clarification, please dont hesitate to reach out to us. Were here to
assist you.
</p>
<div style={{ fontSize: 13, color: '#888', marginBottom: 24 }}>
You received this email because you applied as a seller on the Mercur marketplace.<br />
If you have any questions, please contact our support team.
</div>
<div style={{ marginTop: 32 }}>
<div>Best regards,</div>
<div style={{ fontWeight: 600 }}>The Mercur Team</div>
<div style={{ color: '#888', marginTop: 4 }}>mercur.js</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,34 @@
interface EmailTemplateProps {
data: {
user_name: string,
}
}
export const SellerAccountSubmissionEmailTemplate: React.FC<Readonly<EmailTemplateProps>> = ({ data }) => {
return (
<div style={{
maxWidth: 600,
margin: '0 auto',
fontFamily: 'Arial, sans-serif',
color: '#222',
background: '#fff',
padding: 24,
borderRadius: 10
}}>
<h1>Hello, {data.user_name} 👋</h1>
<p>We are thrilled about your interest in collaborating with us.</p>
<p>
Your application is currently being reviewed by our team. Please expect a response within [three] business days.
If your submission meets our criteria and is accepted, you will receive a confirmation email from us.
</p>
<p>
In the meantime, if you have any questions or need further assistance, feel free to reach out to us.
</p>
<div style={{ marginTop: 32 }}>
<div>Best regards,</div>
<div style={{ fontWeight: 600 }}>The Mercur Team</div>
<div style={{ color: '#888', marginTop: 4 }}>mercur.js</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,83 @@
interface EmailTemplateProps {
data: {
user_name: string,
order_address: string,
order_id: string,
order: {
id: string,
display_id: string,
trackingNumber: string,
items: {
name: string
}[],
currency_code: string,
item_total: number,
shipping_methods: {
amount: number,
name: string
}[],
total: number
email: string
shipping_address: {
first_name: string,
last_name: string,
company: string,
address_1: string,
address_2: string,
city: string,
province: string,
postal_code: string,
phone: string
}
}
}
}
export const SellerCanceledOrderEmailTemplate: React.FC<Readonly<EmailTemplateProps>> = ({ data }) => {
return (
<div style={{
maxWidth: 600,
margin: '0 auto',
fontFamily: 'Arial, sans-serif',
color: '#222',
background: '#fff',
padding: 24,
borderRadius: 10
}}>
<h1>
An order #{data.order.display_id} has been cancelled.
</h1>
<div style={{ marginBottom: 24 }}>
<a
href={data.order_address}
style={{
display: 'inline-block',
padding: '10px 24px',
background: '#222',
color: '#fff',
borderRadius: 6,
textDecoration: 'none',
fontWeight: 600,
marginBottom: 8
}}
>
View Order Details
</a>
<div style={{ fontSize: 13, color: '#555', marginTop: 8 }}>
If you cant click the button, heres your link: <br />
<span style={{ color: '#0070f3' }}>{data.order_address}</span>
</div>
</div>
<p>
If you have any questions, please contact our support team.
</p>
<div style={{ marginTop: 32 }}>
<div>Best regards,</div>
<div style={{ fontWeight: 600 }}>The Mercur Team</div>
<div style={{ color: '#888', marginTop: 4 }}>mercurjs.com</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,103 @@
interface EmailTemplateProps {
data: {
order: {
id: string,
display_id: number | string,
items: any[],
customer: {
first_name: string,
last_name: string,
id: string
},
seller: {
email: string,
name: string,
id: string
}
}
}
}
export const SellerNewOrderEmailTemplate: React.FC<Readonly<EmailTemplateProps>> = ({ data }) => {
const { order } = data;
return (
<div style={{
maxWidth: 600,
margin: '0 auto',
fontFamily: 'Arial, sans-serif',
color: '#222',
background: '#fff',
padding: 24,
borderRadius: 10
}}>
<h1 style={{ fontSize: '2rem', marginBottom: 8 }}>
Hello, {order.seller.name}!
<br />
You have received a new order!
</h1>
<p style={{ fontSize: '1.1rem', marginBottom: 24 }}>
Order <b>#{order.display_id}</b> has just been placed by {order.customer.first_name} {order.customer.last_name}.
</p>
<h3 style={{ marginTop: 32, marginBottom: 12 }}>Order items:</h3>
<table style={{ width: '100%', borderCollapse: 'collapse', marginBottom: 32 }}>
<thead>
<tr>
<th style={{ textAlign: 'left', padding: '8px', borderBottom: '1px solid #eee' }}>Product</th>
<th style={{ textAlign: 'right', padding: '8px', borderBottom: '1px solid #eee' }}>Amount</th>
<th style={{ textAlign: 'right', padding: '8px', borderBottom: '1px solid #eee' }}>Qty</th>
<th style={{ textAlign: 'right', padding: '8px', borderBottom: '1px solid #eee' }}>Total</th>
</tr>
</thead>
<tbody>
{order.items.map((item: any, idx: number) => (
<tr key={item.id || idx} style={{ borderBottom: '1px solid #f3f3f3' }}>
<td style={{ padding: '12px 8px', verticalAlign: 'top' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
{item.thumbnail && (
<img
src={item.thumbnail}
alt={item.product_title}
style={{
width: 48,
height: 48,
objectFit: 'cover',
borderRadius: 6,
marginRight: 12,
border: '1px solid #eee'
}}
/>
)}
<div>
<div style={{ fontWeight: 600 }}>{item.product_title}</div>
<div style={{ fontSize: 13, color: '#555' }}>
Variant: {item.variant_title}
</div>
</div>
</div>
</td>
<td style={{ textAlign: 'right', padding: '12px 8px', verticalAlign: 'top' }}>
{item.unit_price} eur
</td>
<td style={{ textAlign: 'right', padding: '12px 8px', verticalAlign: 'top' }}>
{item.quantity}
</td>
<td style={{ textAlign: 'right', padding: '12px 8px', verticalAlign: 'top' }}>
{item.unit_price * item.quantity} eur
</td>
</tr>
))}
</tbody>
</table>
<div style={{ fontSize: 13, color: '#888', marginBottom: 24 }}>
You received this email because you are a seller on the Mercur marketplace.<br />
If you have any questions, please contact our support team.
</div>
<div style={{ marginTop: 32 }}>
<div>Best regards,</div>
<div style={{ fontWeight: 600 }}>The Mercur Team</div>
<div style={{ color: '#888', marginTop: 4 }}>mercur.js</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,123 @@
interface EmailTemplateProps {
data: {
seller: {
email: string;
name: string;
};
payouts: {
id: string;
created_at: Date;
amount: number;
currency_code: string;
order: {
id: string;
display_id: number;
created_at: Date;
};
}[];
};
}
export const SellerPayoutSummaryEmailTemplate: React.FC<
Readonly<EmailTemplateProps>
> = ({ data }) => {
const { seller, payouts } = data;
return (
<div
style={{
maxWidth: 600,
margin: "0 auto",
fontFamily: "Arial, sans-serif",
color: "#222",
background: "#fff",
padding: 24,
borderRadius: 10,
}}
>
<h1 style={{ fontSize: "2rem", marginBottom: 8 }}>
Hello, {seller.name}!
<br />
You have received new transfers to your Stripe account!
</h1>
<h3 style={{ marginTop: 32, marginBottom: 12 }}>Transfer list:</h3>
<table
style={{ width: "100%", borderCollapse: "collapse", marginBottom: 32 }}
>
<thead>
<tr>
<th
style={{
textAlign: "left",
padding: "8px",
borderBottom: "1px solid #eee",
}}
>
Order
</th>
<th
style={{
textAlign: "right",
padding: "8px",
borderBottom: "1px solid #eee",
}}
>
Amount
</th>
<th
style={{
textAlign: "right",
padding: "8px",
borderBottom: "1px solid #eee",
}}
>
Date
</th>
</tr>
</thead>
<tbody>
{payouts.map((payout, idx) => (
<tr key={idx} style={{ borderBottom: "1px solid #f3f3f3" }}>
<td style={{ padding: "12px 8px", verticalAlign: "top" }}>
<div style={{ display: "flex", alignItems: "center" }}>
<div style={{ fontWeight: 600 }}>
Order #{payout.order.display_id}
</div>
</div>
</td>
<td
style={{
textAlign: "right",
padding: "12px 8px",
verticalAlign: "top",
}}
>
{payout.amount} {payout.currency_code}
</td>
<td
style={{
textAlign: "right",
padding: "12px 8px",
verticalAlign: "top",
}}
>
{payout.order.created_at.toISOString()}
</td>
</tr>
))}
</tbody>
</table>
<div style={{ fontSize: 13, color: "#888", marginBottom: 24 }}>
You received this email because you are a seller on the Mercur
marketplace.
<br />
If you have any questions, please contact our support team.
</div>
<div style={{ marginTop: 32 }}>
<div>Best regards,</div>
<div style={{ fontWeight: 600 }}>The Mercur Team</div>
<div style={{ color: "#888", marginTop: 4 }}>mercur.js</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,31 @@
interface EmailTemplateProps {
data: {
product_title: string
}
}
export const SellerProductApprovedEmailTemplate: React.FC<Readonly<EmailTemplateProps>> = ({ data }) => {
return (
<div style={{
maxWidth: 600,
margin: '0 auto',
fontFamily: 'Arial, sans-serif',
color: '#222',
background: '#fff',
padding: 24,
borderRadius: 10
}}>
<h1 style={{ fontSize: '2rem', marginBottom: 8 }}>
Hello <span role="img" aria-label="wave">👋</span>
</h1>
<p style={{ fontSize: '1.1rem', marginBottom: 16 }}>
Were happy to let you know that your product {data.product_title} has been approved by the administrator.
</p>
<div style={{ marginTop: 32 }}>
<div>Best regards,</div>
<div style={{ fontWeight: 600 }}>The Mercur Team</div>
<div style={{ color: '#888', marginTop: 4 }}>mercur.js</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,31 @@
interface EmailTemplateProps {
data: {
product_title: string
}
}
export const SellerProductRejectedEmailTemplate: React.FC<Readonly<EmailTemplateProps>> = ({ data }) => {
return (
<div style={{
maxWidth: 600,
margin: '0 auto',
fontFamily: 'Arial, sans-serif',
color: '#222',
background: '#fff',
padding: 24,
borderRadius: 10
}}>
<h1 style={{ fontSize: '2rem', marginBottom: 8 }}>
Hello <span role="img" aria-label="wave">👋</span>
</h1>
<h1 style={{ fontSize: '2rem', marginBottom: 8 }}>
We regret to inform you that your product {data.product_title} has been rejected.
</h1>
<div style={{ marginTop: 32 }}>
<div>Best regards,</div>
<div style={{ fontWeight: 600 }}>The Mercur Team</div>
<div style={{ color: '#888', marginTop: 4 }}>mercur.js</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,64 @@
interface EmailTemplateProps {
data: {
user_name: string,
host: string,
order_id: string,
order: {
id: string,
display_id: string,
trackingNumber: string,
items: any[],
currency_code: string,
item_total: number,
shipping_methods: {
amount: number,
name: string
}[],
total: number
email: string
shipping_address: {
first_name: string,
last_name: string,
company: string,
address_1: string,
address_2: string,
city: string,
province: string,
postal_code: string,
phone: string
}
}
}
}
export const SellerOrderShippingEmailTemplate: React.FC<Readonly<EmailTemplateProps>> = ({ data }) => {
return (
<div>
<h1>The order #{data.order.display_id} has been marked as shipped.</h1>
<p>
<div>
<p>
<strong>Delivery Address:</strong>
</p>
<p>
{data.order.shipping_address.first_name} {data.order.shipping_address.last_name}
,<br />
{data.order.shipping_address?.company ? `${data.order.shipping_address.company}, ` : ''}
{data.order.shipping_address.address_1}
{data.order.shipping_address.address_2}, {data.order.shipping_address.postal_code}{' '}
{data.order.shipping_address.city}
{data.order.shipping_address.province ? `, ${data.order.shipping_address.province}` : ''}
<br />
{data.order.email}, {data.order.shipping_address.phone}
</p>
</div>
</p>
<p>Thank you for updating the status of the order. If you have any questions, please contact our support team.</p>
<div style={{ marginTop: 32 }}>
<div>Best regards,</div>
<div style={{ fontWeight: 600 }}>The Mercur Team</div>
<div style={{ color: '#888', marginTop: 4 }}>mercurjs.com</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,69 @@
interface EmailTemplateProps {
data: {
user_name: string;
store_name: string;
host: string;
id: string;
email: string;
};
}
export const SellerTeamInviteEmailTemplate: React.FC<
Readonly<EmailTemplateProps>
> = ({ data }) => {
return (
<div
style={{
maxWidth: 600,
margin: "0 auto",
fontFamily: "Arial, sans-serif",
color: "#222",
background: "#fff",
padding: 24,
borderRadius: 10,
}}
>
<h1 style={{ fontSize: "2rem", marginBottom: "8px" }}>
{data.user_name} has invited you to join the team at {data.store_name}.
</h1>
<p style={{ fontSize: "1.1rem", marginBottom: "16px" }}>
To join the team at <b>{data.store_name}</b>, please accept the
invitation.
<br />
Your login email: <b>{data.email}</b>
</p>
<div style={{ marginBottom: 24 }}>
<a
href={`${data.host}`}
style={{
display: "inline-block",
padding: "10px 24px",
background: "#222",
color: "#fff",
borderRadius: 6,
textDecoration: "none",
fontWeight: 600,
marginBottom: 8,
}}
>
Accept Invitation
</a>
<div style={{ fontSize: 13, color: "#555", marginTop: 8 }}>
If you cant click the button, heres your link: <br />
<span style={{ color: "#0070f3" }}>{`${data.host}`}</span>
</div>
</div>
<div style={{ fontSize: 13, color: "#888", marginBottom: 24 }}>
You received this email because you were invited to join a team on the
Mercur marketplace.
<br />
If you have any questions, please contact our support team.
</div>
<div style={{ marginTop: 32 }}>
<div>Best regards,</div>
<div style={{ fontWeight: 600 }}>The Mercur Team</div>
<div style={{ color: "#888", marginTop: 4 }}>mercur.js</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,28 @@
interface EmailTemplateProps {
data: {
user_name: string
link: string
}
}
export const SellerEmailVerifyEmailTemplate: React.FC<Readonly<EmailTemplateProps>> = ({ data }) => {
return (
<div>
<h1>
Hello, {data.user_name} 👋
<br />
Thank you for submiting your account to Mercur Marketplace
</h1>
<p>
Before we proceed with account submission there is but one last thing to do - verify your email.
Please <a href={data.link}>click here</a> to verify your email.
</p>
<p>Thank you for choosing us. We are excited about you joining us and will support you every step of the way.</p>
<div style={{ marginTop: 32 }}>
<div>Best regards,</div>
<div style={{ fontWeight: 600 }}>The Mercur Team</div>
<div style={{ color: '#888', marginTop: 4 }}>mercurjs.com</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,8 @@
import { ModuleProvider, Modules } from "@medusajs/framework/utils";
import ResendService from "./service";
export * from "./types";
export default ModuleProvider(Modules.NOTIFICATION, {
services: [ResendService],
});

View File

@@ -0,0 +1,62 @@
import { Resend } from 'resend'
import {
AbstractNotificationProviderService,
MedusaError
} from '@medusajs/framework/utils'
import { ProviderSendNotificationDTO } from '@medusajs/types'
import { emailTemplates } from './email-templates'
type ResendOptions = {
api_key: string
from: string
}
class ResendNotificationProviderService extends AbstractNotificationProviderService {
static identifier = 'notification-resend'
private resendClient: Resend
private options: ResendOptions
constructor(_, options: ResendOptions) {
super()
this.validateModuleOptions(options)
this.resendClient = new Resend(options.api_key)
this.options = options
}
validateModuleOptions(options: ResendOptions) {
for (const key in options) {
if (!options[key]) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`No ${key} was provided in the ${ResendNotificationProviderService.identifier} options. Please add one.`
)
}
}
}
async send(notification: ProviderSendNotificationDTO) {
const { data, error } = await this.resendClient.emails.send({
from: notification.from?.trim() || this.options.from,
to: notification.to,
subject: notification.content?.subject as string,
react: emailTemplates[notification.template](notification.data)
})
if (error) {
throw new MedusaError(MedusaError.Types.UNEXPECTED_STATE, error.message)
}
if (!data) {
throw new MedusaError(
MedusaError.Types.UNEXPECTED_STATE,
'No data returned by resend client'
)
}
return data
}
}
export default ResendNotificationProviderService

View File

@@ -0,0 +1 @@
export * from "./templates";

View File

@@ -0,0 +1,21 @@
export enum ResendNotificationTemplates {
BUYER_ACCOUNT_CREATED = "buyerAccountCreatedEmailTemplate",
BUYER_NEW_ORDER = "buyerNewOrderEmailTemplate",
BUYER_CANCELED_ORDER = "buyerCancelOrderEmailTemplate",
BUYER_ORDER_SHIPPED = "buyerOrderShippedEmailTemplate",
SELLER_ACCOUNT_UPDATES_SUBMISSION = "sellerAccountSubmissionEmailTemplate",
SELLER_ACCOUNT_UPDATES_APPROVAL = "sellerAccountApprovedEmailTemplate",
SELLER_ACCOUNT_UPDATES_REJECTION = "sellerAccountRejectedEmailTemplate",
SELLER_NEW_ORDER = "sellerNewOrderEmailTemplate",
SELLER_CANCELED_ORDER = "sellerCanceledOrderEmailTemplate",
SELLER_ORDER_SHIPPED = "sellerOrderShippingEmailTemplate",
SELLER_TEAM_MEMBER_INVITATION = "sellerTeamInviteEmailTemplate",
SELLER_VERIFY_EMAIL_TEMPLATE = "sellerVerifyEmailTemplate",
FORGOT_PASSWORD = "forgotPasswordEmailTemplate",
NEW_SELLER_INVITATION = "newSellerInvitation",
SELLER_PRODUCT_APPROVED = "sellerProductApprovedEmailTemplate",
SELLER_PRODUCT_REJECTED = "sellerProductRejectedEmailTemplate",
ADMIN_REQUEST_CREATED = "adminRequestCreatedEmailTemplate",
ADMIN_SELLER_REQUEST_CREATED = "adminSellerRequestCreatedEmailTemplate",
SELLER_PAYOUT_SUMMARY = "sellerPayoutSummaryEmailTemplate",
}

View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"lib": ["ES2021"],
"target": "ES2021",
"outDir": "${configDir}/dist",
"jsx": "react-jsx",
"esModuleInterop": true,
"declaration": true,
"declarationMap": true,
"noUnusedLocals": true,
"module": "node16",
"moduleResolution": "node16",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": true,
"noImplicitReturns": true,
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"allowJs": true,
"skipLibCheck": true,
"incremental": false
},
"include": ["${configDir}/src"],
"exclude": ["${configDir}/dist", "${configDir}/node_modules"]
}