nitro-web 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/.editorconfig +9 -0
  2. package/.eslintrc.json +86 -0
  3. package/_example/.env-example +16 -0
  4. package/_example/client/config.ts +5 -0
  5. package/_example/client/css/index.css +35 -0
  6. package/_example/client/fonts/Roboto-Bold.ttf +0 -0
  7. package/_example/client/fonts/Roboto-BoldItalic.ttf +0 -0
  8. package/_example/client/fonts/Roboto-Italic.ttf +0 -0
  9. package/_example/client/fonts/Roboto-Medium.ttf +0 -0
  10. package/_example/client/fonts/Roboto-MediumItalic.ttf +0 -0
  11. package/_example/client/fonts/Roboto-Regular.ttf +0 -0
  12. package/_example/client/fonts/inter-v13-latin-300.woff2 +0 -0
  13. package/_example/client/fonts/inter-v13-latin-500.woff2 +0 -0
  14. package/_example/client/fonts/inter-v13-latin-600.woff2 +0 -0
  15. package/_example/client/fonts/inter-v13-latin-700.woff2 +0 -0
  16. package/_example/client/fonts/inter-v13-latin-800.woff2 +0 -0
  17. package/_example/client/fonts/inter-v13-latin-900.woff2 +0 -0
  18. package/_example/client/fonts/inter-v13-latin-regular.woff2 +0 -0
  19. package/_example/client/imgs/android-chrome-512x512.png +0 -0
  20. package/_example/client/imgs/favicon.png +0 -0
  21. package/_example/client/imgs/icons/calendar.svg +3 -0
  22. package/_example/client/imgs/icons/email.svg +6 -0
  23. package/_example/client/imgs/icons/eye-open.svg +4 -0
  24. package/_example/client/imgs/icons/eye.svg +5 -0
  25. package/_example/client/imgs/icons/filter.svg +7 -0
  26. package/_example/client/imgs/icons/left-circle.svg +3 -0
  27. package/_example/client/imgs/icons/left.svg +3 -0
  28. package/_example/client/imgs/icons/line-options.svg +5 -0
  29. package/_example/client/imgs/icons/line.svg +3 -0
  30. package/_example/client/imgs/icons/person.svg +7 -0
  31. package/_example/client/imgs/icons/plus-circle.svg +5 -0
  32. package/_example/client/imgs/icons/plus.svg +5 -0
  33. package/_example/client/imgs/icons/right-circle.svg +3 -0
  34. package/_example/client/imgs/icons/right.svg +3 -0
  35. package/_example/client/imgs/icons/search.svg +3 -0
  36. package/_example/client/imgs/icons/shield.svg +6 -0
  37. package/_example/client/imgs/icons/tick-circle-solid.svg +8 -0
  38. package/_example/client/imgs/icons/tick-circle.svg +6 -0
  39. package/_example/client/imgs/icons/tick.svg +5 -0
  40. package/_example/client/imgs/icons/up2-small.svg +4 -0
  41. package/_example/client/imgs/icons/up2.svg +4 -0
  42. package/_example/client/imgs/icons/updown.svg +6 -0
  43. package/_example/client/imgs/icons/v-big-dark.svg +3 -0
  44. package/_example/client/imgs/icons/v-dark.svg +3 -0
  45. package/_example/client/imgs/icons/v.svg +3 -0
  46. package/_example/client/imgs/icons/v2-active.svg +6 -0
  47. package/_example/client/imgs/icons/x1.svg +4 -0
  48. package/_example/client/imgs/logo/logo-white.svg +20 -0
  49. package/_example/client/imgs/logo/logo.svg +20 -0
  50. package/_example/client/imgs/no-image.jpg +0 -0
  51. package/_example/client/imgs/user.jpg +0 -0
  52. package/_example/client/index.html +12 -0
  53. package/_example/client/index.ts +47 -0
  54. package/_example/components/auth.api.js +1 -0
  55. package/_example/components/index.tsx +225 -0
  56. package/_example/components/partials/layouts.tsx +5 -0
  57. package/_example/components/settings.api.js +1 -0
  58. package/_example/server/config.js +120 -0
  59. package/_example/server/email/welcome.html +27 -0
  60. package/_example/server/index.js +32 -0
  61. package/_example/tailwind.config.js +84 -0
  62. package/_example/tsconfig.json +32 -0
  63. package/_example/types.d.ts +7 -0
  64. package/_example/webpack.config.js +4 -0
  65. package/client/app.js +300 -0
  66. package/client/css/components.css +84 -0
  67. package/client/css/fonts.css +67 -0
  68. package/client/imgs/icons/calendar.svg +3 -0
  69. package/client/imgs/icons/email.svg +6 -0
  70. package/client/imgs/icons/eye-open.svg +4 -0
  71. package/client/imgs/icons/eye.svg +5 -0
  72. package/client/imgs/icons/filter.svg +7 -0
  73. package/client/imgs/icons/left-circle.svg +3 -0
  74. package/client/imgs/icons/left.svg +3 -0
  75. package/client/imgs/icons/line-options.svg +5 -0
  76. package/client/imgs/icons/line.svg +3 -0
  77. package/client/imgs/icons/person.svg +7 -0
  78. package/client/imgs/icons/plus-circle.svg +5 -0
  79. package/client/imgs/icons/plus.svg +5 -0
  80. package/client/imgs/icons/right-circle.svg +3 -0
  81. package/client/imgs/icons/right.svg +3 -0
  82. package/client/imgs/icons/search.svg +3 -0
  83. package/client/imgs/icons/shield.svg +6 -0
  84. package/client/imgs/icons/tick-circle-solid.svg +8 -0
  85. package/client/imgs/icons/tick-circle.svg +6 -0
  86. package/client/imgs/icons/tick.svg +5 -0
  87. package/client/imgs/icons/up2-small.svg +4 -0
  88. package/client/imgs/icons/up2.svg +4 -0
  89. package/client/imgs/icons/updown.svg +6 -0
  90. package/client/imgs/icons/v-big-dark.svg +3 -0
  91. package/client/imgs/icons/v-dark.svg +3 -0
  92. package/client/imgs/icons/v.svg +3 -0
  93. package/client/imgs/icons/v2-active.svg +6 -0
  94. package/client/imgs/icons/x1.svg +4 -0
  95. package/client.js +42 -0
  96. package/components/auth/auth.api.js +419 -0
  97. package/components/auth/reset.jsx +88 -0
  98. package/components/auth/signin.jsx +74 -0
  99. package/components/auth/signup.jsx +62 -0
  100. package/components/billing/stripe.api.js +267 -0
  101. package/components/partials/element/accordion.jsx +82 -0
  102. package/components/partials/element/avatar.jsx +28 -0
  103. package/components/partials/element/button.jsx +66 -0
  104. package/components/partials/element/dropdown.jsx +185 -0
  105. package/components/partials/element/initials.jsx +56 -0
  106. package/components/partials/element/message.jsx +124 -0
  107. package/components/partials/element/modal.jsx +229 -0
  108. package/components/partials/element/sidebar.jsx +166 -0
  109. package/components/partials/element/tooltip.jsx +146 -0
  110. package/components/partials/element/topbar.jsx +25 -0
  111. package/components/partials/form/checkbox.jsx +74 -0
  112. package/components/partials/form/drop-handler.jsx +62 -0
  113. package/components/partials/form/drop.jsx +125 -0
  114. package/components/partials/form/form-error.jsx +21 -0
  115. package/components/partials/form/input-color.jsx +77 -0
  116. package/components/partials/form/input-currency.jsx +133 -0
  117. package/components/partials/form/input-date.jsx +223 -0
  118. package/components/partials/form/input.jsx +131 -0
  119. package/components/partials/form/location.jsx +212 -0
  120. package/components/partials/form/select.jsx +369 -0
  121. package/components/partials/form/toggle.jsx +46 -0
  122. package/components/partials/is-first-render.js +15 -0
  123. package/components/partials/layout/layout1.jsx +32 -0
  124. package/components/partials/layout/layout2.jsx +47 -0
  125. package/components/partials/not-found.jsx +7 -0
  126. package/components/partials/styleguide.jsx +252 -0
  127. package/components/settings/settings-account.jsx +143 -0
  128. package/components/settings/settings-business.jsx +121 -0
  129. package/components/settings/settings-team--member.jsx +108 -0
  130. package/components/settings/settings-team.jsx +76 -0
  131. package/components/settings/settings.api.js +54 -0
  132. package/package.json +175 -0
  133. package/readme.md +43 -0
  134. package/server/email/index.js +192 -0
  135. package/server/email/partials/email.css +153 -0
  136. package/server/email/partials/layout1.swig +92 -0
  137. package/server/email/partials/line.swig +8 -0
  138. package/server/email/partials/vert-10.swig +8 -0
  139. package/server/email/partials/vert-15.swig +8 -0
  140. package/server/email/partials/vert-20.swig +8 -0
  141. package/server/email/partials/vert-25.swig +8 -0
  142. package/server/email/partials/vert-30.swig +8 -0
  143. package/server/email/partials/vert-35.swig +8 -0
  144. package/server/email/partials/vert-50.swig +8 -0
  145. package/server/email/reset-password.html +21 -0
  146. package/server/email/welcome.html +21 -0
  147. package/server/models/company.js +76 -0
  148. package/server/models/user.js +45 -0
  149. package/server/router.js +355 -0
  150. package/server.js +20 -0
  151. package/util.js +1145 -0
  152. package/webpack.config.js +302 -0
@@ -0,0 +1,76 @@
1
+ // todo: finish tailwind conversion
2
+ import * as util from '../../util.js'
3
+ import SvgPlus from '../../client/imgs/icons/plus.svg'
4
+ import { Button } from '../partials/element/button.jsx'
5
+ import { Table } from '../partials/element/table.jsx'
6
+ import { Avatar } from '../partials/element/avatar.jsx'
7
+ import { Tabbar } from '../partials/element/tabbar.jsx'
8
+ import { Topbar } from '../partials/element/topbar.jsx'
9
+ import { SettingsTeamMember } from './settings-team--member.jsx'
10
+
11
+ export function SettingsTeam({ config }) {
12
+ const isLoading = useState('')
13
+ const [showModal, setShowModal] = useState()
14
+ const [{ user }] = sharedStore.useTracked()
15
+ const [state] = useState({
16
+ users: user?.company?.users || [],
17
+ })
18
+
19
+ function addTeamMember() {
20
+ //... open modal
21
+ }
22
+
23
+ return (
24
+ <div>
25
+ <Topbar
26
+ title={<>Settings</>}
27
+ submenu={
28
+ <Tabbar class="is-underline" tabs={[
29
+ { label: 'Business', path: '/settings/business' },
30
+ { label: 'Team', path: '/settings/team' },
31
+ { label: 'Account', path: '/settings/account' },
32
+ ]} />
33
+ }
34
+ btns={
35
+ <Button onClick={addTeamMember} color="primary-sm" IconLeft={SvgPlus} isLoading={isLoading[0]}>
36
+ Add Team Member
37
+ </Button>
38
+ }
39
+ />
40
+
41
+ <Table
42
+ columns={[
43
+ { label: 'Member\'s Name', key: 'name', width: 1 },
44
+ { label: 'Email', key: 'email' },
45
+ { label: 'Joined On', key: 'joinedOn', align: 'center' },
46
+ { label: 'Role', key: 'role', width: '110px' },
47
+ ]}
48
+ rowOnClick={(e, user) => setShowModal(user)}
49
+ rows={
50
+ state.users.map(user => ({
51
+ ...user,
52
+ key: user._id,
53
+ name: (
54
+ <>
55
+ <Avatar awsUrl={config.awsUrl} user={user} isRound={true} class="mt--1 mb--1" />
56
+ <b>{util.ucFirst(user.name)}</b>
57
+ {user.status != 'invited' && <span class="text-grey">(Invitation pending)</span>}
58
+ </>
59
+ ),
60
+ joinedOn: user.status == 'invited' ? <a href="#">Resend Invite</a> : util.date(user.createdAt),
61
+ role: util.ucFirst(user.role),
62
+ }))
63
+ }
64
+ actions={[
65
+ { label: 'Remove', onClick: (_row, _i) => console.log('remove') },
66
+ ]}
67
+ actionsAll={[
68
+ { label: 'Remove All', onClick: () => console.log('remove all') },
69
+ ]}
70
+ />
71
+
72
+ {/* Member modal */}
73
+ <SettingsTeamMember showModal={showModal} setShowModal={setShowModal} />
74
+ </div>
75
+ )
76
+ }
@@ -0,0 +1,54 @@
1
+ import db from 'monastery'
2
+
3
+ export default {
4
+
5
+ routes: {
6
+ 'put /api/company/:cid': ['isCompanyUser', 'update'],
7
+ 'put /api/user/:uid': ['isUser', 'updateUser'],
8
+ },
9
+
10
+ update: async function(req, res) {
11
+ try {
12
+ const update = await db.company.update({
13
+ query: req.params.cid,
14
+ data: req.body,
15
+ files: req.query.files ? req.files : undefined,
16
+ })
17
+ if (!update) {
18
+ throw new Error('Coudln\'t find the company to update')
19
+ }
20
+ const company = await db.company.findOne({
21
+ query: req.params.cid,
22
+ populate: db.company.loginPopulate(),
23
+ _privateData: true,
24
+ })
25
+ res.json(company)
26
+
27
+ } catch (errs) {
28
+ res.error(errs)
29
+ }
30
+ },
31
+
32
+ updateUser: async function(req, res) {
33
+ try {
34
+ const update = await db.user.update({
35
+ query: req.params.uid,
36
+ data: req.body,
37
+ files: req.query.files ? req.files : undefined,
38
+ })
39
+ if (!update) {
40
+ throw new Error('Coudln\'t find the user to update')
41
+ }
42
+ const user = await db.user.findOne({
43
+ query: req.params.uid,
44
+ _privateData: true,
45
+ blacklist: ['company'], // don't return the company id
46
+ })
47
+ res.json(user)
48
+
49
+ } catch (errs) {
50
+ res.error(errs)
51
+ }
52
+ },
53
+
54
+ }
package/package.json ADDED
@@ -0,0 +1,175 @@
1
+ {
2
+ "name": "nitro-web",
3
+ "description": "Nitro is a battle-tested, modular base project to turbocharge your projects, styled using Tailwind 🚀",
4
+ "repository": "github:boycce/nitro-web",
5
+ "version": "0.0.1",
6
+ "main": "client.js",
7
+ "type": "module",
8
+ "keywords": [
9
+ "express",
10
+ "javascript",
11
+ "monastery",
12
+ "mongodb",
13
+ "react",
14
+ "tailwind",
15
+ "webpack"
16
+ ],
17
+ "imports": {
18
+ "#nitro-web/*": { "default": "./*" }
19
+ },
20
+ "babelMacros": {
21
+ "twin": {
22
+ "preset": "emotion",
23
+ "config": "./_example/tailwind.config.js"
24
+ }
25
+ },
26
+ "scripts": {
27
+ "build": "NODE_ENV=production webpack --target=web",
28
+ "dev": "clear && npm run dev:server:lint --silent & npm run dev:server --silent & npm run dev:client --silent",
29
+ "dev:client": "NITRO=true webpack serve --progress --config ./_example/webpack.config.js",
30
+ "dev:server": "NITRO=true nodemon ./_example/server -q -w ./_example/server/ -w ./_example/components/ -w ./server/ -w ./components/ -e js",
31
+ "dev:server:lint": "eslint ./_example/components ./_example/server ./components/ ./server/",
32
+ "major": "standard-version --release-as major && npm publish",
33
+ "minor": "standard-version --release-as minor && npm publish",
34
+ "patch": "standard-version --release-as patch && npm publish",
35
+ "start": "node ./_example/server",
36
+ "stripe": "stripe listen --forward-to localhost:3001/api/stripe/webhook"
37
+ },
38
+ "dependencies": {
39
+ "@emotion/react": "^11.11.4",
40
+ "@headlessui/react": "^2.2.0",
41
+ "@heroicons/react": "^2.2.0",
42
+ "@hokify/axios": "^0.19.1",
43
+ "@stripe/react-stripe-js": "^1.9.0",
44
+ "@stripe/stripe-js": "^1.34.0",
45
+ "@uiw/color-convert": "^2.3.0",
46
+ "@uiw/react-color-hue": "^2.3.0",
47
+ "@uiw/react-color-saturation": "^2.3.0",
48
+ "axios-retry": "^3.3.1",
49
+ "bcrypt": "^5.0.0",
50
+ "body-parser": "^1.19.0",
51
+ "chart.js": "^4.4.3",
52
+ "compression": "^1.7.4",
53
+ "connect-mongo": "^5.1.0",
54
+ "date-fns": "^3.6.0",
55
+ "dateformat": "^3.0.3",
56
+ "dotenv": "^14.3.2",
57
+ "express": "^4.17.1",
58
+ "express-fileupload": "^1.1.6",
59
+ "express-session": "^1.17.0",
60
+ "inline-css": "^4.0.2",
61
+ "monastery": "~3.5.1",
62
+ "nanoid": "^4.0.0",
63
+ "nodemailer": "^6.5.0",
64
+ "nodemailer-mailgun-transport": "^2.0.2",
65
+ "nunjucks": "^3.2.2",
66
+ "passport": "^0.4.1",
67
+ "passport-jwt": "^4.0.0",
68
+ "passport-local": "^1.0.0",
69
+ "pdf-to-img": "^4.1.0",
70
+ "pdfmake": "0.2.7",
71
+ "react": "^18.3.1",
72
+ "react-chartjs-2": "^5.2.0",
73
+ "react-currency-input-field": "^3.8.0",
74
+ "react-day-picker": "^8.10.1",
75
+ "react-dom": "^18.3.1",
76
+ "react-number-format": "^5.4.0",
77
+ "react-router-dom": "6.24.1",
78
+ "react-select": "^5.9.0",
79
+ "react-tracked": "^1.3.0",
80
+ "sort-route-addresses-nodeps": "0.0.4",
81
+ "stripe": "^9.16.0"
82
+ },
83
+ "devDependencies": {
84
+ "@babel/core": "^7.8.0",
85
+ "@babel/plugin-syntax-dynamic-import": "^7.8.3",
86
+ "@babel/plugin-transform-runtime": "^7.17.0",
87
+ "@babel/preset-env": "^7.8.0",
88
+ "@babel/preset-react": "^7.9.4",
89
+ "@babel/preset-typescript": "^7.24.7",
90
+ "@emotion/babel-plugin": "^11.11.0",
91
+ "@emotion/eslint-plugin": "^11.11.0",
92
+ "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
93
+ "@svgr/webpack": "^8.1.0",
94
+ "@types/react": "^19.0.2",
95
+ "@types/react-dom": "^19.0.2",
96
+ "@typescript-eslint/eslint-plugin": "^8.18.1",
97
+ "@typescript-eslint/parser": "^8.18.1",
98
+ "autoprefixer": "^9.8.8",
99
+ "babel-eslint": "^10.0.3",
100
+ "babel-loader": "^8.0.6",
101
+ "babel-plugin-macros": "^3.1.0",
102
+ "babel-plugin-react-html-attrs": "^2.1.0",
103
+ "clean-terminal-webpack-plugin": "https://github.com/boycce/clean-terminal-webpack-plugin.git",
104
+ "color": "^4.2.3",
105
+ "copy-webpack-plugin": "^12.0.2",
106
+ "core-js": "^3.12.1",
107
+ "css-loader": "^3.6.0",
108
+ "csv-loader": "^3.0.5",
109
+ "eslint": "^8.57.1",
110
+ "eslint-plugin-import": "^2.26.0",
111
+ "eslint-plugin-jest": "^28.9.0",
112
+ "eslint-plugin-react": "^7.19.0",
113
+ "eslint-plugin-react-hooks": "^4.0.0",
114
+ "eslint-webpack-plugin": "^2.7.0",
115
+ "html-loader": "^1.3.2",
116
+ "html-webpack-plugin": "^5.6.0",
117
+ "jest": "^29.7.0",
118
+ "migrate-mongo": "^10.0.0",
119
+ "mini-css-extract-plugin": "^2.9.0",
120
+ "nodemon": "^2.0.1",
121
+ "postcss": "^8.4.49",
122
+ "postcss-for": "^2.1.1",
123
+ "postcss-import": "^16.1.0",
124
+ "postcss-import-resolver": "^2.0.0",
125
+ "postcss-loader": "^8.1.1",
126
+ "postcss-nested": "^7.0.2",
127
+ "react-refresh": "^0.14.2",
128
+ "standard-version": "^9.3.2",
129
+ "string-replace-loader": "^3.1.0",
130
+ "super-nunjucks-loader": "^2.0.0",
131
+ "tailwind-merge": "^2.6.0",
132
+ "tailwindcss": "^3.4.17",
133
+ "twin.macro": "^3.4.1",
134
+ "typescript": "^5.5.2",
135
+ "webpack": "^5.92.1",
136
+ "webpack-cli": "^5.1.4",
137
+ "webpack-dev-server": "^4.6.0",
138
+ "webpack-node-externals": "^1.7.2"
139
+ },
140
+ "engines": {
141
+ "node": "^18"
142
+ },
143
+ "browserslist": [
144
+ "> 1%",
145
+ "last 2 versions",
146
+ "not ie <= 10"
147
+ ],
148
+ "nodemonConfig": {
149
+ "events": {
150
+ "restart": "echo \"\\033[0;35mExpress restarting..\\033[0m\""
151
+ }
152
+ },
153
+ "standard-version": {
154
+ "releaseCommitMessageFormat": "{{currentTag}}",
155
+ "sign": true,
156
+ "skip": {
157
+ "changelog": true,
158
+ "tag": true
159
+ }
160
+ },
161
+ "prettier": {
162
+ "printWidth": 140,
163
+ "quoteProps": "consistent",
164
+ "semi": false,
165
+ "singleQuote": true,
166
+ "tabWidth": 2,
167
+ "trailingComma": "es5",
168
+ "useTabs": false
169
+ },
170
+ "jest": {
171
+ "transform": {}
172
+ },
173
+ "author": "",
174
+ "license": "ISC"
175
+ }
package/readme.md ADDED
@@ -0,0 +1,43 @@
1
+ # Nitro
2
+
3
+ [![NPM](https://img.shields.io/npm/v/nitro-web.svg)](https://www.npmjs.com/package/nitro-web) [![Build Status](https://travis-ci.com/boycce/nitro-web.svg?branch=master)](https://app.travis-ci.com/github/boycce/nitro-web)
4
+
5
+ Nitro is a battle-tested, modular base project to turbocharge your projects, styled using Tailwind 🚀
6
+
7
+ ### Install
8
+
9
+ ```bash
10
+ npm i nitro-web
11
+ ```
12
+
13
+ ### Setup
14
+
15
+ 1. Copy the `./_example` folder to your project
16
+ 2. Copy over `./package.json`
17
+ 4. In `./package.json`, search and replace `/_example` with `./`
18
+ 5. In `./package.json`, replace `{ "default": "./*" }` with `{ "default": "nitro-web/*" }`
19
+ 6. `npm i`
20
+
21
+ ### Running in development
22
+
23
+ ```bash
24
+ npm run dev:server # run and watch the nodemon server
25
+ npm run dev:client # run and watch the webpack dev server
26
+ npm run dev # or, run and watch both server and client
27
+ ```
28
+
29
+ ### Building for production
30
+
31
+ ```bash
32
+ npm run build
33
+ npm run start
34
+ ```
35
+
36
+ ### Versions
37
+
38
+ - Express `^4.17`
39
+ - Monastery `~3.5.1`
40
+ - Node `^18`
41
+ - React `^18.3`
42
+ - Tailwind `^3.4`
43
+ - Webpack `^5.92`
@@ -0,0 +1,192 @@
1
+ // import axios from '@hokify/axios'
2
+ import nodemailer from 'nodemailer'
3
+ import mailgun from 'nodemailer-mailgun-transport'
4
+ import nunjucks from 'nunjucks'
5
+ import inlineCss from 'inline-css'
6
+ import { dirname, join } from 'path'
7
+ import { fileURLToPath } from 'url'
8
+
9
+ let templates = {}
10
+ let nodemailerMailgun = undefined
11
+ const _dirname = dirname(fileURLToPath(import.meta.url)) + '/'
12
+
13
+
14
+ export async function sendEmail({ template, to, bcc, data={}, from, replyTo, recipientVariables, subject, test, skipCssInline, config }) {
15
+ /**
16
+ * Email recipient a predefined template with data and/or recipientVariables
17
+ *
18
+ * @param {string} template = e.g. 'reset-password' or html
19
+ * @param {string} to - e.g. "Bruce Lee<bruce@gmail.com>, ..."
20
+ * @param {object} config - e.g. { mailgunKey, mailgunDomain, emailFrom, clientUrl }
21
+ * @param {string} <bcc> - e.g. "Chuck Norris<chuck@gmail.com>" (not sent in development)
22
+ * @param {object} <data> - recipientVariables[to] shorthand
23
+ * @param {string} <from> - e.g. "Chuck Norris<chuck@gmail.com>"
24
+ * @param {string} <replyTo> - e.g. "Chuck Norris<chuck@gmail.com>"
25
+ * @param {object} <recipientVariables> - mailgun recipient-variables for batch sending
26
+ * @param {string} <subject> - subject, this can also be defined in the template
27
+ * @param {boolean} <skipCssInline> - skip inlining css
28
+ * @param {boolean} <test> - subject, this can also be defined in the template
29
+ * @return Promise([mailgunErr, mailgunInfo])
30
+ */
31
+ if (!config) {
32
+ throw new Error('sendEmail: `config` missing')
33
+ } else if (!config.emailFrom || !config.clientUrl) {
34
+ throw new Error('sendEmail: `config.emailFrom` or `config.clientUrl` is missing')
35
+ } else if (!test && (!config.mailgunKey || !config.mailgunDomain)) {
36
+ throw new Error('sendEmail: `config.mailgunKey` or `config.mailgunDomain` is missing')
37
+ } else if (!template) {
38
+ throw new Error('sendEmail: `template` missing')
39
+ } else if (!to) {
40
+ throw new Error('sendEmail: `to` missing')
41
+ }
42
+
43
+ // Setup nodemailer once
44
+ if (!nodemailerMailgun && !test) {
45
+ nodemailerMailgun = nodemailer.createTransport(
46
+ mailgun({ auth: { api_key: config.mailgunKey, domain: config.mailgunDomain }})
47
+ )
48
+ }
49
+
50
+ // From, replayTo
51
+ from = from || config.emailFrom
52
+ replyTo = replyTo || config.emailReplyTo || from
53
+
54
+ // Data is recipientVariables[to] shorthand
55
+ if (data) {
56
+ recipientVariables = { [getNameEmail(to)[1]]: data }
57
+ }
58
+
59
+ // Add default recipientVariables
60
+ for (let toEmail in recipientVariables) {
61
+ recipientVariables[toEmail] = {
62
+ domain: config.clientUrl,
63
+ email: toEmail,
64
+ greet: data.name || getNameEmail(to)[0]? 'Hi ' + (data.name || getNameEmail(to)[0]) : 'Hello',
65
+ name: getNameEmail(to)[0],
66
+ replyToEmail: getNameEmail(replyTo)[1],
67
+ replyToName: getNameEmail(replyTo)[0],
68
+ ...recipientVariables[toEmail],
69
+ }
70
+ }
71
+
72
+ let settings = {
73
+ bcc: bcc,
74
+ from: from,
75
+ isDev: config.clientUrl.match(/:/),
76
+ recipientVariables: recipientVariables,
77
+ replyTo: replyTo,
78
+ skipCssInline: skipCssInline,
79
+ subject: subject,
80
+ template: template,
81
+ test: config.emailTestMode || test,
82
+ to: to,
83
+ url: config.clientUrl,
84
+ }
85
+
86
+ // Grab html and send
87
+ let html = template.match('<') ? template : await getTemplate(settings, config)
88
+ if (!html) throw new Error('Sendmail: No template returned from getTemplate(..)')
89
+ return await sendWithMailgun(settings, html) // note, mailgun errors are resolved
90
+ }
91
+
92
+ async function getTemplate(settings, config) {
93
+ try {
94
+ var templateName = settings.template
95
+ if (!templates[templateName] || settings.isDev) {
96
+ nunjucks.configure({ noCache: config.env === 'development' })
97
+ // Setup the nunjucks environment
98
+ let env = new nunjucks.Environment([
99
+ new nunjucks.FileSystemLoader(`${config.emailTemplateDir}`), // user templates take precedence
100
+ new nunjucks.FileSystemLoader(`${_dirname}`), // then fallback to nitro default templates
101
+ ])
102
+ // Get the template
103
+ let template = env.getTemplate(templateName + '.html', true)
104
+ // Render the template
105
+ let html = template.render({})
106
+ if (settings.skipCssInline && settings.test) {
107
+ templates[templateName] = html
108
+ } else {
109
+ try {
110
+ // First try to inline the CSS from the user templates directory (config.emailTemplateDir)
111
+ templates[templateName] = await inlineCssForPath(html, config.emailTemplateDir)
112
+ } catch (e) {
113
+ // If the CSS is not found, use default nitro CSS file
114
+ if (templateName == 'reset-password' || templateName == 'welcome') {
115
+ templates[templateName] = await inlineCssForPath(html, `${_dirname}`)
116
+ } else {
117
+ throw e
118
+ }
119
+ }
120
+ }
121
+ return templates[templateName]
122
+ } else {
123
+ return templates[templateName]
124
+ }
125
+ } catch (e) {
126
+ console.error(`Sendmail: issue retrieving the email template "${templateName}.html"`)
127
+ console.error(e)
128
+ throw e
129
+ }
130
+ }
131
+
132
+ async function inlineCssForPath(html, path) {
133
+ const url = join('file://', path)
134
+ return await inlineCss(html, { url })
135
+ }
136
+
137
+ function getNameEmail(nameEmail) {
138
+ // Splits 'Bruce<bruce@gmail.com>' into [name, email]
139
+ nameEmail = nameEmail.split(',')[0]
140
+ var name = nameEmail.match('<')? nameEmail.split('<')[0] : ''
141
+ var email = nameEmail.match('<')? nameEmail.split(/<|>/g)[1] : nameEmail
142
+ return [name, email]
143
+ }
144
+
145
+ function processTemplate(settings, html) {
146
+ // If sending to only one email, replace mailgun placeholders before they reach mailgun (handy for testing templates)
147
+ if (!settings.to.match(/,/) && Object.keys(settings.recipientVariables).length) {
148
+ const recipientVariables = settings.recipientVariables[Object.keys(settings.recipientVariables)[0]]
149
+ for (let key in recipientVariables) {
150
+ html = html.replace(new RegExp('%recipient\\.' + key + '%', 'g'), recipientVariables[key])
151
+ }
152
+ }
153
+ // Extract the subject from the template
154
+ let foundSubject = html.match(new RegExp('\\[\\[\\s*subject\\s*=\\s*(.*?)\\s*\\]\\]'))
155
+ html = html.replace(new RegExp('\\[\\[\\s*subject\\s*=.*?\\]\\]'), '')
156
+ // Save subject (if called from an instance)
157
+ if (!settings.subject) {
158
+ if (foundSubject) settings.subject = foundSubject[1]
159
+ else throw new Error('Sendmail: please pass `subject` or set it in the template')
160
+ }
161
+ return html
162
+ }
163
+
164
+ async function sendWithMailgun(settings, html) {
165
+ // Supports batch sending via recipientVariables, limit 1000 emails
166
+ // https://documentation.mailgun.com/en/latest/user_manual.html?highlight=batch%20sending#batch-sending
167
+ let processedhtml = await processTemplate(settings, html)
168
+ if (settings.test) return processedhtml
169
+
170
+ return new Promise((resolve, reject) => {
171
+ nodemailerMailgun.sendMail({
172
+ ...(settings.bcc && !settings.isDev? { bcc: settings.bcc } : {}),
173
+ from: settings.from,
174
+ html: processedhtml,
175
+ 'h:Reply-To': settings.replyTo,
176
+ subject: settings.subject,
177
+ to: settings.to,
178
+ ...(!settings.recipientVariables? {} : {
179
+ 'recipient-variables': typeof settings.recipientVariables == 'string'
180
+ ? settings.recipientVariables
181
+ : JSON.stringify(settings.recipientVariables),
182
+ }),
183
+ }, function(err, info) {
184
+ if (err) {
185
+ console.error('SendEmail mailgun error')
186
+ reject(err)
187
+ } else {
188
+ resolve(info)
189
+ }
190
+ })
191
+ })
192
+ }
@@ -0,0 +1,153 @@
1
+ /* Reset */
2
+
3
+ div,
4
+ p {
5
+ margin: 0!important;
6
+ }
7
+ table {
8
+ border-collapse: collapse;
9
+ }
10
+ td[class=textBox] {
11
+ padding-left: 30px;
12
+ padding-right: 30px;
13
+ }
14
+ ul {
15
+ margin: 11px 0 0 28px;
16
+ text-align: left;
17
+ }
18
+
19
+ /* Layout */
20
+
21
+ .email-body {
22
+ width: 100%;
23
+ background-color: #ffffff;
24
+ margin: 0;
25
+ padding: 0;
26
+ font-family: Inter, 'Arial', 'Open Sans', Helvetica, Arial;
27
+ }
28
+ .table-header {
29
+ background-color: #000000;
30
+ }
31
+ .table-header td td:first-child {
32
+ padding-left: 40px;
33
+ }
34
+ .table-header td td:last-child {
35
+ padding-right: 40px;
36
+ }
37
+ .table-header img {
38
+ vertical-align: top;
39
+ }
40
+ .table-body {
41
+ border: 1px solid #f2f2f2;
42
+ }
43
+ .table-content {
44
+ width: 100%;
45
+ }
46
+
47
+ /* Content */
48
+
49
+ .content {
50
+ font-size: 15.5px;
51
+ /* font-size: 16px; */
52
+ font-style: normal;
53
+ font-weight: 400;
54
+ color: #111;
55
+ line-height: 1.7;
56
+ mso-line-height-rule: exactly;
57
+ }
58
+ .content a {
59
+ color: #000;
60
+ text-decoration: underline;
61
+ font-style: normal;
62
+ }
63
+ .content a:hover {
64
+ text-decoration: underline;
65
+ font-style: normal;
66
+ }
67
+ .content a.button {
68
+ color: #fff;
69
+ font-weight: 600;
70
+ line-height: 22px;
71
+ font-size: 13px;
72
+ text-decoration: none;
73
+ background-color: #3a3942;
74
+ border-top: 11px solid #3a3942;
75
+ border-bottom: 11px solid #3a3942;
76
+ border-left: 50px solid #3a3942;
77
+ border-right: 50px solid #3a3942;
78
+ border-radius: 5px;
79
+ text-align: center;
80
+ display: inline-block;
81
+ }
82
+ .content b {
83
+ color: #000000;
84
+ font-weight: 600;
85
+ }
86
+ .small {
87
+ color: #8e8e8e;
88
+ font-size: 11px;
89
+ text-transform: uppercase;
90
+ }
91
+
92
+ /* Responsive layout */
93
+
94
+ @media (min-resolution: 144dpi),
95
+ (-webkit-min-device-pixel-ratio: 1.5),
96
+ (min--moz-device-pixel-ratio: 1.5),
97
+ (-o-min-device-pixel-ratio: 1.5/1),
98
+ (min-device-pixel-ratio: 1.5),
99
+ (min-resolution: 1.5dppx) {}
100
+ @media only screen and (max-width:640px) {
101
+ /*table table,
102
+ td[class=full_width] {
103
+ width:100%!important;
104
+ }*/
105
+ div[class=div-scale],
106
+ table[class=table-scale],
107
+ td[class=td-scale] {
108
+ width: 440px!important;
109
+ margin: 0 auto!important;
110
+ }
111
+ img[class=img-scale] {
112
+ width: 100%!important;
113
+ height: auto!important;
114
+ }
115
+ table[class=spacer],
116
+ td[class=spacer] {}
117
+ td[class=center] {
118
+ text-align: center!important;
119
+ }
120
+ table[class=full] {
121
+ width: 400px!important;
122
+ margin-left: 20px!important;
123
+ margin-right: 20px!important;
124
+ }
125
+ }
126
+ @media only screen and (max-width:479px) {
127
+ div[class=div-scale],
128
+ table[class=table-scale],
129
+ td[class=td-scale] {
130
+ width: 320px!important;
131
+ margin: 0 auto!important;
132
+ }
133
+ img[class=img-scale] {
134
+ width: 100%!important;
135
+ height: auto!important;
136
+ }
137
+ table[class=spacer],
138
+ td[class=spacer] {
139
+ display: none!important;
140
+ }
141
+ td[class=center] {
142
+ text-align: center!important;
143
+ }
144
+ td[class=textBox] {
145
+ padding-left: 10px!important;
146
+ padding-right: 10px!important;
147
+ }
148
+ table[class=full] {
149
+ width: 320px!important;
150
+ margin-left: 20px!important;
151
+ margin-right: 20px!important;
152
+ }
153
+ }