forked from tsai/budibase
committed by
GitHub
47 changed files with 2593 additions and 2210 deletions
@ -0,0 +1,22 @@ |
|||
<script> |
|||
import { |
|||
Body, |
|||
Menu, |
|||
MenuItem, |
|||
Detail, |
|||
MenuSection, |
|||
DetailSummary, |
|||
} from "@budibase/bbui" |
|||
|
|||
export let bindings |
|||
export let onBindingClick = () => {} |
|||
</script> |
|||
|
|||
<Menu> |
|||
{#each bindings as binding} |
|||
<MenuItem on:click={() => onBindingClick(binding)}> |
|||
<Detail size="M">{binding.name}</Detail> |
|||
<Body size="XS" noPadding>{binding.description}</Body> |
|||
</MenuItem> |
|||
{/each} |
|||
</Menu> |
|||
@ -0,0 +1,13 @@ |
|||
<script> |
|||
import { goto } from "@roxi/routify" |
|||
|
|||
export let value |
|||
</script> |
|||
|
|||
<span on:click={() => $goto(`./${value}`)}>{value}</span> |
|||
|
|||
<style> |
|||
span { |
|||
text-transform: capitalize; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,142 @@ |
|||
<script> |
|||
import { |
|||
Menu, |
|||
MenuItem, |
|||
Button, |
|||
Detail, |
|||
Heading, |
|||
Divider, |
|||
Label, |
|||
Modal, |
|||
ModalContent, |
|||
notifications, |
|||
Layout, |
|||
Icon, |
|||
Body, |
|||
Page, |
|||
Select, |
|||
Tabs, |
|||
Tab, |
|||
MenuSection, |
|||
MenuSeparator, |
|||
} from "@budibase/bbui" |
|||
import { goto } from "@roxi/routify" |
|||
import { onMount } from "svelte" |
|||
import { fade } from "svelte/transition" |
|||
import { email } from "stores/portal" |
|||
import Editor from "components/integration/QueryEditor.svelte" |
|||
import TemplateBindings from "./TemplateBindings.svelte" |
|||
import api from "builderStore/api" |
|||
|
|||
const ConfigTypes = { |
|||
SMTP: "smtp", |
|||
} |
|||
|
|||
export let template |
|||
|
|||
let selected = "Edit" |
|||
let selectedBindingTab = "Template" |
|||
let htmlEditor |
|||
|
|||
$: selectedTemplate = $email.templates.find( |
|||
({ purpose }) => purpose === template |
|||
) |
|||
$: templateBindings = |
|||
$email.definitions?.bindings[selectedTemplate.purpose] || [] |
|||
|
|||
async function saveTemplate() { |
|||
try { |
|||
// Save your template config |
|||
await email.templates.save(selectedTemplate) |
|||
notifications.success(`Template saved.`) |
|||
} catch (err) { |
|||
notifications.error(`Failed to update template settings. ${err}`) |
|||
} |
|||
} |
|||
|
|||
function setTemplateBinding(binding) { |
|||
htmlEditor.update((selectedTemplate.contents += `{{ ${binding.name} }}`)) |
|||
} |
|||
</script> |
|||
|
|||
<Page wide gap="L"> |
|||
<div class="backbutton" on:click={() => $goto("./")}> |
|||
<Icon name="BackAndroid" /> |
|||
<span>Back</span> |
|||
</div> |
|||
<header> |
|||
<Heading> |
|||
Email Template: {template} |
|||
</Heading> |
|||
<Button cta on:click={saveTemplate}>Save</Button> |
|||
</header> |
|||
<Tabs {selected}> |
|||
<Tab title="Edit"> |
|||
<div class="template-editor"> |
|||
<Editor |
|||
editorHeight={800} |
|||
bind:this={htmlEditor} |
|||
mode="handlebars" |
|||
on:change={e => { |
|||
selectedTemplate.contents = e.detail.value |
|||
}} |
|||
value={selectedTemplate.contents} |
|||
/> |
|||
<div class="bindings-editor"> |
|||
<Detail size="L">Bindings</Detail> |
|||
<Tabs selected={selectedBindingTab}> |
|||
<Tab title="Template"> |
|||
<TemplateBindings |
|||
title="Template Bindings" |
|||
bindings={templateBindings} |
|||
onBindingClick={setTemplateBinding} |
|||
/> |
|||
</Tab> |
|||
<Tab title="Common"> |
|||
<TemplateBindings |
|||
title="Common Bindings" |
|||
bindings={$email.definitions.bindings.common} |
|||
onBindingClick={setTemplateBinding} |
|||
/> |
|||
</Tab> |
|||
</Tabs> |
|||
</div> |
|||
</div></Tab |
|||
> |
|||
<Tab title="Preview"> |
|||
<div class="preview" transition:fade> |
|||
{@html selectedTemplate.contents} |
|||
</div> |
|||
</Tab> |
|||
</Tabs> |
|||
</Page> |
|||
|
|||
<style> |
|||
.template-editor { |
|||
display: grid; |
|||
grid-template-columns: 1fr 20%; |
|||
grid-gap: var(--spacing-xl); |
|||
margin-top: var(--spacing-xl); |
|||
} |
|||
|
|||
header { |
|||
display: flex; |
|||
width: 100%; |
|||
justify-content: space-between; |
|||
margin-bottom: var(--spacing-l); |
|||
margin-top: var(--spacing-l); |
|||
} |
|||
|
|||
.preview { |
|||
background: white; |
|||
height: 800px; |
|||
padding: var(--spacing-xl); |
|||
} |
|||
|
|||
.backbutton { |
|||
display: flex; |
|||
gap: var(--spacing-m); |
|||
margin-bottom: var(--spacing-xl); |
|||
cursor: pointer; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,191 @@ |
|||
<script> |
|||
import { goto } from "@roxi/routify" |
|||
import { |
|||
Menu, |
|||
MenuItem, |
|||
Button, |
|||
Heading, |
|||
Divider, |
|||
Label, |
|||
Modal, |
|||
ModalContent, |
|||
notifications, |
|||
Layout, |
|||
Input, |
|||
TextArea, |
|||
Body, |
|||
Page, |
|||
Select, |
|||
MenuSection, |
|||
MenuSeparator, |
|||
Table, |
|||
} from "@budibase/bbui" |
|||
import { onMount } from "svelte" |
|||
import { email } from "stores/portal" |
|||
import Editor from "components/integration/QueryEditor.svelte" |
|||
import TemplateBindings from "./TemplateBindings.svelte" |
|||
import TemplateLink from "./TemplateLink.svelte" |
|||
import api from "builderStore/api" |
|||
|
|||
const ConfigTypes = { |
|||
SMTP: "smtp", |
|||
} |
|||
|
|||
const templateSchema = { |
|||
purpose: { |
|||
displayName: "Email", |
|||
editable: false, |
|||
}, |
|||
} |
|||
|
|||
const customRenderers = [ |
|||
{ |
|||
column: "purpose", |
|||
component: TemplateLink, |
|||
}, |
|||
] |
|||
|
|||
let smtpConfig |
|||
let bindingsOpen = false |
|||
let htmlModal |
|||
let htmlEditor |
|||
let loading |
|||
|
|||
async function saveSmtp() { |
|||
try { |
|||
// Save your SMTP config |
|||
const response = await api.post(`/api/admin/configs`, smtpConfig) |
|||
const json = await response.json() |
|||
if (response.status !== 200) throw new Error(json.message) |
|||
smtpConfig._rev = json._rev |
|||
smtpConfig._id = json._id |
|||
|
|||
notifications.success(`Settings saved.`) |
|||
} catch (err) { |
|||
notifications.error(`Failed to save email settings. ${err}`) |
|||
} |
|||
} |
|||
|
|||
async function saveTemplate() { |
|||
try { |
|||
await email.templates.save(selectedTemplate) |
|||
notifications.success(`Template saved.`) |
|||
} catch (err) { |
|||
notifications.error(`Failed to update template settings. ${err}`) |
|||
} |
|||
} |
|||
|
|||
async function fetchSmtp() { |
|||
// fetch the configs for smtp |
|||
const smtpResponse = await api.get(`/api/admin/configs/${ConfigTypes.SMTP}`) |
|||
const smtpDoc = await smtpResponse.json() |
|||
|
|||
if (!smtpDoc._id) { |
|||
smtpConfig = { |
|||
type: ConfigTypes.SMTP, |
|||
config: { |
|||
auth: { |
|||
type: "login", |
|||
}, |
|||
}, |
|||
} |
|||
} else { |
|||
smtpConfig = smtpDoc |
|||
} |
|||
} |
|||
|
|||
onMount(async () => { |
|||
loading = true |
|||
await fetchSmtp() |
|||
await email.templates.fetch() |
|||
loading = false |
|||
}) |
|||
</script> |
|||
|
|||
<Page> |
|||
<header> |
|||
<Heading size="M">Email</Heading> |
|||
<Body size="S"> |
|||
Sending email is not required, but highly recommended for processes such |
|||
as password recovery. To setup automated auth emails, simply add the |
|||
values below and click activate. |
|||
</Body> |
|||
</header> |
|||
<Divider /> |
|||
{#if smtpConfig} |
|||
<div class="config-form"> |
|||
<Heading size="S">SMTP</Heading> |
|||
<Body size="S"> |
|||
To allow your app to benefit from automated auth emails, add your SMTP |
|||
details below. |
|||
</Body> |
|||
<Layout gap="S"> |
|||
<Heading size="S"> |
|||
<span /> |
|||
</Heading> |
|||
<div class="form-row"> |
|||
<Label>Host</Label> |
|||
<Input bind:value={smtpConfig.config.host} /> |
|||
</div> |
|||
<div class="form-row"> |
|||
<Label>Port</Label> |
|||
<Input type="number" bind:value={smtpConfig.config.port} /> |
|||
</div> |
|||
<div class="form-row"> |
|||
<Label>User</Label> |
|||
<Input bind:value={smtpConfig.config.auth.user} /> |
|||
</div> |
|||
<div class="form-row"> |
|||
<Label>Password</Label> |
|||
<Input type="password" bind:value={smtpConfig.config.auth.pass} /> |
|||
</div> |
|||
<div class="form-row"> |
|||
<Label>From email address</Label> |
|||
<Input type="email" bind:value={smtpConfig.config.from} /> |
|||
</div> |
|||
</Layout> |
|||
<Button cta on:click={saveSmtp}>Save</Button> |
|||
</div> |
|||
<Divider /> |
|||
|
|||
<div class="config-form"> |
|||
<Heading size="S">Templates</Heading> |
|||
<Body size="S"> |
|||
Budibase comes out of the box with ready-made email templates to help |
|||
with user onboarding. Please refrain from changing the links. |
|||
</Body> |
|||
</div> |
|||
<Table |
|||
{customRenderers} |
|||
data={$email.templates} |
|||
schema={templateSchema} |
|||
{loading} |
|||
allowEditRows={false} |
|||
allowSelectRows={false} |
|||
allowEditColumns={false} |
|||
/> |
|||
{/if} |
|||
</Page> |
|||
|
|||
<style> |
|||
.config-form { |
|||
margin-top: 42px; |
|||
margin-bottom: 42px; |
|||
} |
|||
|
|||
.form-row { |
|||
display: grid; |
|||
grid-template-columns: 20% 1fr; |
|||
grid-gap: var(--spacing-l); |
|||
} |
|||
|
|||
span { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: var(--spacing-s); |
|||
} |
|||
|
|||
header { |
|||
margin-bottom: 42px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,44 @@ |
|||
import { writable } from "svelte/store" |
|||
import api from "builderStore/api" |
|||
|
|||
export function createEmailStore() { |
|||
const store = writable([]) |
|||
|
|||
return { |
|||
subscribe: store.subscribe, |
|||
templates: { |
|||
fetch: async () => { |
|||
// fetch the email template definitions
|
|||
const response = await api.get(`/api/admin/template/definitions`) |
|||
const definitions = await response.json() |
|||
|
|||
// fetch the email templates themselves
|
|||
const templatesResponse = await api.get(`/api/admin/template/email`) |
|||
const templates = await templatesResponse.json() |
|||
|
|||
store.set({ |
|||
definitions, |
|||
templates, |
|||
}) |
|||
}, |
|||
save: async template => { |
|||
// Save your template config
|
|||
const response = await api.post(`/api/admin/template`, template) |
|||
const json = await response.json() |
|||
if (response.status !== 200) throw new Error(json.message) |
|||
template._rev = json._rev |
|||
template._id = json._id |
|||
|
|||
store.update(state => { |
|||
const currentIdx = state.templates.findIndex( |
|||
template => template.purpose === json.purpose |
|||
) |
|||
state.templates.splice(currentIdx, 1, template) |
|||
return state |
|||
}) |
|||
}, |
|||
}, |
|||
} |
|||
} |
|||
|
|||
export const email = createEmailStore() |
|||
@ -1,3 +1,4 @@ |
|||
export { organisation } from "./organisation" |
|||
export { admin } from "./admin" |
|||
export { apps } from "./apps" |
|||
export { email } from "./email" |
|||
|
|||
File diff suppressed because it is too large
@ -0,0 +1,67 @@ |
|||
const { sendSmtpEmail } = require("../../utilities/workerRequests") |
|||
|
|||
module.exports.definition = { |
|||
description: "Send an email using SMTP", |
|||
tagline: "Send SMTP email to {{inputs.to}}", |
|||
icon: "ri-mail-open-line", |
|||
name: "Send Email (SMTP)", |
|||
type: "ACTION", |
|||
stepId: "SEND_EMAIL_SMTP", |
|||
inputs: {}, |
|||
schema: { |
|||
inputs: { |
|||
properties: { |
|||
to: { |
|||
type: "string", |
|||
title: "Send To", |
|||
}, |
|||
from: { |
|||
type: "string", |
|||
title: "Send From", |
|||
}, |
|||
subject: { |
|||
type: "string", |
|||
title: "Email Subject", |
|||
}, |
|||
contents: { |
|||
type: "string", |
|||
title: "HTML Contents", |
|||
}, |
|||
}, |
|||
required: ["to", "from", "subject", "contents"], |
|||
}, |
|||
outputs: { |
|||
properties: { |
|||
success: { |
|||
type: "boolean", |
|||
description: "Whether the email was sent", |
|||
}, |
|||
response: { |
|||
type: "object", |
|||
description: "A response from the email client, this may be an error", |
|||
}, |
|||
}, |
|||
required: ["success"], |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
module.exports.run = async function ({ inputs }) { |
|||
let { to, from, subject, contents } = inputs |
|||
if (!contents) { |
|||
contents = "<h1>No content</h1>" |
|||
} |
|||
try { |
|||
let response = await sendSmtpEmail(to, from, subject, contents) |
|||
return { |
|||
success: true, |
|||
response, |
|||
} |
|||
} catch (err) { |
|||
console.error(err) |
|||
return { |
|||
success: false, |
|||
response: err, |
|||
} |
|||
} |
|||
} |
|||
@ -1,8 +1,8 @@ |
|||
module.exports.definition = { |
|||
description: "Send an email", |
|||
description: "Send an email using SendGrid", |
|||
tagline: "Send email to {{inputs.to}}", |
|||
icon: "ri-mail-open-line", |
|||
name: "Send Email", |
|||
name: "Send Email (SendGrid)", |
|||
type: "ACTION", |
|||
stepId: "SEND_EMAIL", |
|||
inputs: {}, |
|||
File diff suppressed because it is too large
@ -0,0 +1,93 @@ |
|||
const authPkg = require("@budibase/auth") |
|||
const { google } = require("@budibase/auth/src/middleware") |
|||
const { Configs } = require("../../constants") |
|||
const CouchDB = require("../../db") |
|||
const { clearCookie } = authPkg.utils |
|||
const { Cookies } = authPkg.constants |
|||
const { passport } = authPkg.auth |
|||
|
|||
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name |
|||
|
|||
exports.authenticate = async (ctx, next) => { |
|||
return passport.authenticate("local", async (err, user) => { |
|||
if (err) { |
|||
return ctx.throw(403, "Unauthorized") |
|||
} |
|||
|
|||
const expires = new Date() |
|||
expires.setDate(expires.getDate() + 1) |
|||
|
|||
if (!user) { |
|||
return ctx.throw(403, "Unauthorized") |
|||
} |
|||
|
|||
ctx.cookies.set(Cookies.Auth, user.token, { |
|||
expires, |
|||
path: "/", |
|||
httpOnly: false, |
|||
overwrite: true, |
|||
}) |
|||
|
|||
delete user.token |
|||
|
|||
ctx.body = { user } |
|||
})(ctx, next) |
|||
} |
|||
|
|||
exports.logout = async ctx => { |
|||
clearCookie(ctx, Cookies.Auth) |
|||
ctx.body = { message: "User logged out" } |
|||
} |
|||
|
|||
/** |
|||
* The initial call that google authentication makes to take you to the google login screen. |
|||
* On a successful login, you will be redirected to the googleAuth callback route. |
|||
*/ |
|||
exports.googlePreAuth = async (ctx, next) => { |
|||
const db = new CouchDB(GLOBAL_DB) |
|||
const config = await authPkg.db.getScopedFullConfig(db, { |
|||
type: Configs.GOOGLE, |
|||
group: ctx.query.group, |
|||
}) |
|||
const strategy = await google.strategyFactory(config) |
|||
|
|||
return passport.authenticate(strategy, { |
|||
scope: ["profile", "email"], |
|||
})(ctx, next) |
|||
} |
|||
|
|||
exports.googleAuth = async (ctx, next) => { |
|||
const db = new CouchDB(GLOBAL_DB) |
|||
|
|||
const config = await authPkg.db.getScopedFullConfig(db, { |
|||
type: Configs.GOOGLE, |
|||
group: ctx.query.group, |
|||
}) |
|||
const strategy = await google.strategyFactory(config) |
|||
|
|||
return passport.authenticate( |
|||
strategy, |
|||
{ successRedirect: "/", failureRedirect: "/error" }, |
|||
async (err, user) => { |
|||
if (err) { |
|||
return ctx.throw(403, "Unauthorized") |
|||
} |
|||
|
|||
const expires = new Date() |
|||
expires.setDate(expires.getDate() + 1) |
|||
|
|||
if (!user) { |
|||
return ctx.throw(403, "Unauthorized") |
|||
} |
|||
|
|||
ctx.cookies.set(Cookies.Auth, user.token, { |
|||
expires, |
|||
path: "/", |
|||
httpOnly: false, |
|||
overwrite: true, |
|||
}) |
|||
|
|||
ctx.redirect("/") |
|||
} |
|||
)(ctx, next) |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
<tr> |
|||
<td class="email-body" width="570" cellpadding="0" cellspacing="0"> |
|||
<table class="email-body_inner" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation"> |
|||
<!-- Body content --> |
|||
<tr> |
|||
<td class="content-cell"> |
|||
<div class="f-fallback"> |
|||
{{ contents }} |
|||
</div> |
|||
</td> |
|||
</tr> |
|||
</table> |
|||
</td> |
|||
</tr> |
|||
@ -1,408 +0,0 @@ |
|||
/* Based on templates: https://github.com/wildbit/postmark-templates/blob/master/templates/plain */ |
|||
/* Base ------------------------------ */ |
|||
|
|||
@import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro&display=swap'); |
|||
body { |
|||
width: 100% !important; |
|||
height: 100%; |
|||
margin: 0; |
|||
-webkit-text-size-adjust: none; |
|||
} |
|||
|
|||
a { |
|||
color: #3869D4; |
|||
} |
|||
|
|||
a img { |
|||
border: none; |
|||
} |
|||
|
|||
td { |
|||
word-break: break-word; |
|||
} |
|||
|
|||
.preheader { |
|||
display: none !important; |
|||
visibility: hidden; |
|||
mso-hide: all; |
|||
font-size: 1px; |
|||
line-height: 1px; |
|||
max-height: 0; |
|||
max-width: 0; |
|||
opacity: 0; |
|||
overflow: hidden; |
|||
} |
|||
/* Type ------------------------------ */ |
|||
|
|||
body, |
|||
td, |
|||
th { |
|||
font-family: "Source Sans Pro", Helvetica, Arial, sans-serif; |
|||
} |
|||
|
|||
h1 { |
|||
margin-top: 0; |
|||
color: #333333; |
|||
font-size: 22px; |
|||
font-weight: bold; |
|||
text-align: left; |
|||
} |
|||
|
|||
h2 { |
|||
margin-top: 0; |
|||
color: #333333; |
|||
font-size: 16px; |
|||
font-weight: bold; |
|||
text-align: left; |
|||
} |
|||
|
|||
h3 { |
|||
margin-top: 0; |
|||
color: #333333; |
|||
font-size: 14px; |
|||
font-weight: bold; |
|||
text-align: left; |
|||
} |
|||
|
|||
td, |
|||
th { |
|||
font-size: 16px; |
|||
} |
|||
|
|||
p, |
|||
ul, |
|||
ol, |
|||
blockquote { |
|||
margin: .4em 0 1.1875em; |
|||
font-size: 16px; |
|||
line-height: 1.625; |
|||
} |
|||
|
|||
p.sub { |
|||
font-size: 13px; |
|||
} |
|||
/* Utilities ------------------------------ */ |
|||
|
|||
.align-right { |
|||
text-align: right; |
|||
} |
|||
|
|||
.align-left { |
|||
text-align: left; |
|||
} |
|||
|
|||
.align-center { |
|||
text-align: center; |
|||
} |
|||
/* Buttons ------------------------------ */ |
|||
|
|||
.button { |
|||
background-color: #3869D4; |
|||
border-top: 10px solid #3869D4; |
|||
border-right: 18px solid #3869D4; |
|||
border-bottom: 10px solid #3869D4; |
|||
border-left: 18px solid #3869D4; |
|||
display: inline-block; |
|||
color: #FFF; |
|||
text-decoration: none; |
|||
border-radius: 3px; |
|||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16); |
|||
-webkit-text-size-adjust: none; |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
.button--green { |
|||
background-color: #22BC66; |
|||
border-top: 10px solid #22BC66; |
|||
border-right: 18px solid #22BC66; |
|||
border-bottom: 10px solid #22BC66; |
|||
border-left: 18px solid #22BC66; |
|||
} |
|||
|
|||
.button--red { |
|||
background-color: #FF6136; |
|||
border-top: 10px solid #FF6136; |
|||
border-right: 18px solid #FF6136; |
|||
border-bottom: 10px solid #FF6136; |
|||
border-left: 18px solid #FF6136; |
|||
} |
|||
|
|||
@media only screen and (max-width: 500px) { |
|||
.button { |
|||
width: 100% !important; |
|||
text-align: center !important; |
|||
} |
|||
} |
|||
/* Attribute list ------------------------------ */ |
|||
|
|||
.attributes { |
|||
margin: 0 0 21px; |
|||
} |
|||
|
|||
.attributes_content { |
|||
background-color: #F4F4F7; |
|||
padding: 16px; |
|||
} |
|||
|
|||
.attributes_item { |
|||
padding: 0; |
|||
} |
|||
/* Related Items ------------------------------ */ |
|||
|
|||
.related { |
|||
width: 100%; |
|||
margin: 0; |
|||
padding: 25px 0 0 0; |
|||
-premailer-width: 100%; |
|||
-premailer-cellpadding: 0; |
|||
-premailer-cellspacing: 0; |
|||
} |
|||
|
|||
.related_item { |
|||
padding: 10px 0; |
|||
color: #CBCCCF; |
|||
font-size: 15px; |
|||
line-height: 18px; |
|||
} |
|||
|
|||
.related_item-title { |
|||
display: block; |
|||
margin: .5em 0 0; |
|||
} |
|||
|
|||
.related_item-thumb { |
|||
display: block; |
|||
padding-bottom: 10px; |
|||
} |
|||
|
|||
.related_heading { |
|||
border-top: 1px solid #CBCCCF; |
|||
text-align: center; |
|||
padding: 25px 0 10px; |
|||
} |
|||
/* Discount Code ------------------------------ */ |
|||
|
|||
.discount { |
|||
width: 100%; |
|||
margin: 0; |
|||
padding: 24px; |
|||
-premailer-width: 100%; |
|||
-premailer-cellpadding: 0; |
|||
-premailer-cellspacing: 0; |
|||
background-color: #F4F4F7; |
|||
border: 2px dashed #CBCCCF; |
|||
} |
|||
|
|||
.discount_heading { |
|||
text-align: center; |
|||
} |
|||
|
|||
.discount_body { |
|||
text-align: center; |
|||
font-size: 15px; |
|||
} |
|||
/* Social Icons ------------------------------ */ |
|||
|
|||
.social { |
|||
width: auto; |
|||
} |
|||
|
|||
.social td { |
|||
padding: 0; |
|||
width: auto; |
|||
} |
|||
|
|||
.social_icon { |
|||
height: 20px; |
|||
margin: 0 8px 10px 8px; |
|||
padding: 0; |
|||
} |
|||
/* Data table ------------------------------ */ |
|||
|
|||
.purchase { |
|||
width: 100%; |
|||
margin: 0; |
|||
padding: 35px 0; |
|||
-premailer-width: 100%; |
|||
-premailer-cellpadding: 0; |
|||
-premailer-cellspacing: 0; |
|||
} |
|||
|
|||
.purchase_content { |
|||
width: 100%; |
|||
margin: 0; |
|||
padding: 25px 0 0 0; |
|||
-premailer-width: 100%; |
|||
-premailer-cellpadding: 0; |
|||
-premailer-cellspacing: 0; |
|||
} |
|||
|
|||
.purchase_item { |
|||
padding: 10px 0; |
|||
color: #51545E; |
|||
font-size: 15px; |
|||
line-height: 18px; |
|||
} |
|||
|
|||
.purchase_heading { |
|||
padding-bottom: 8px; |
|||
border-bottom: 1px solid #EAEAEC; |
|||
} |
|||
|
|||
.purchase_heading p { |
|||
margin: 0; |
|||
color: #85878E; |
|||
font-size: 12px; |
|||
} |
|||
|
|||
.purchase_footer { |
|||
padding-top: 15px; |
|||
border-top: 1px solid #EAEAEC; |
|||
} |
|||
|
|||
.purchase_total { |
|||
margin: 0; |
|||
text-align: right; |
|||
font-weight: bold; |
|||
color: #333333; |
|||
} |
|||
|
|||
.purchase_total--label { |
|||
padding: 0 15px 0 0; |
|||
} |
|||
|
|||
body { |
|||
background-color: #FFF; |
|||
color: #333; |
|||
} |
|||
|
|||
p { |
|||
color: #333; |
|||
} |
|||
|
|||
.email-wrapper { |
|||
width: 100%; |
|||
margin: 0; |
|||
padding: 0; |
|||
-premailer-width: 100%; |
|||
-premailer-cellpadding: 0; |
|||
-premailer-cellspacing: 0; |
|||
} |
|||
|
|||
.email-content { |
|||
width: 100%; |
|||
margin: 0; |
|||
padding: 0; |
|||
-premailer-width: 100%; |
|||
-premailer-cellpadding: 0; |
|||
-premailer-cellspacing: 0; |
|||
} |
|||
/* Masthead ----------------------- */ |
|||
|
|||
.email-masthead { |
|||
padding: 25px 0; |
|||
text-align: center; |
|||
} |
|||
|
|||
.email-masthead_logo { |
|||
width: 94px; |
|||
} |
|||
|
|||
.email-masthead_name { |
|||
font-size: 16px; |
|||
font-weight: bold; |
|||
color: #A8AAAF; |
|||
text-decoration: none; |
|||
text-shadow: 0 1px 0 white; |
|||
} |
|||
/* Body ------------------------------ */ |
|||
|
|||
.email-body { |
|||
width: 100%; |
|||
margin: 0; |
|||
padding: 0; |
|||
-premailer-width: 100%; |
|||
-premailer-cellpadding: 0; |
|||
-premailer-cellspacing: 0; |
|||
} |
|||
|
|||
.email-body_inner { |
|||
width: 570px; |
|||
margin: 0 auto; |
|||
padding: 0; |
|||
-premailer-width: 570px; |
|||
-premailer-cellpadding: 0; |
|||
-premailer-cellspacing: 0; |
|||
} |
|||
|
|||
.email-footer { |
|||
width: 570px; |
|||
margin: 0 auto; |
|||
padding: 0; |
|||
-premailer-width: 570px; |
|||
-premailer-cellpadding: 0; |
|||
-premailer-cellspacing: 0; |
|||
text-align: center; |
|||
} |
|||
|
|||
.email-footer p { |
|||
color: #A8AAAF; |
|||
} |
|||
|
|||
.body-action { |
|||
width: 100%; |
|||
margin: 30px auto; |
|||
padding: 0; |
|||
-premailer-width: 100%; |
|||
-premailer-cellpadding: 0; |
|||
-premailer-cellspacing: 0; |
|||
text-align: center; |
|||
} |
|||
|
|||
.body-sub { |
|||
margin-top: 25px; |
|||
padding-top: 25px; |
|||
border-top: 1px solid #EAEAEC; |
|||
} |
|||
|
|||
.content-cell { |
|||
padding: 35px; |
|||
} |
|||
/*Media Queries ------------------------------ */ |
|||
|
|||
@media only screen and (max-width: 600px) { |
|||
.email-body_inner, |
|||
.email-footer { |
|||
width: 100% !important; |
|||
} |
|||
} |
|||
|
|||
@media (prefers-color-scheme: dark) { |
|||
body { |
|||
background-color: #333333 !important; |
|||
color: #FFF !important; |
|||
} |
|||
p, |
|||
ul, |
|||
ol, |
|||
blockquote, |
|||
h1, |
|||
h2, |
|||
h3, |
|||
span, |
|||
.purchase_item { |
|||
color: #FFF !important; |
|||
} |
|||
.attributes_content, |
|||
.discount { |
|||
background-color: #222 !important; |
|||
} |
|||
.email-masthead_name { |
|||
text-shadow: none !important; |
|||
} |
|||
} |
|||
|
|||
:root { |
|||
color-scheme: light dark; |
|||
supported-color-schemes: light dark; |
|||
} |
|||
Loading…
Reference in new issue