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.
- package/.editorconfig +9 -0
- package/.eslintrc.json +86 -0
- package/_example/.env-example +16 -0
- package/_example/client/config.ts +5 -0
- package/_example/client/css/index.css +35 -0
- package/_example/client/fonts/Roboto-Bold.ttf +0 -0
- package/_example/client/fonts/Roboto-BoldItalic.ttf +0 -0
- package/_example/client/fonts/Roboto-Italic.ttf +0 -0
- package/_example/client/fonts/Roboto-Medium.ttf +0 -0
- package/_example/client/fonts/Roboto-MediumItalic.ttf +0 -0
- package/_example/client/fonts/Roboto-Regular.ttf +0 -0
- package/_example/client/fonts/inter-v13-latin-300.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-500.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-600.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-700.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-800.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-900.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-regular.woff2 +0 -0
- package/_example/client/imgs/android-chrome-512x512.png +0 -0
- package/_example/client/imgs/favicon.png +0 -0
- package/_example/client/imgs/icons/calendar.svg +3 -0
- package/_example/client/imgs/icons/email.svg +6 -0
- package/_example/client/imgs/icons/eye-open.svg +4 -0
- package/_example/client/imgs/icons/eye.svg +5 -0
- package/_example/client/imgs/icons/filter.svg +7 -0
- package/_example/client/imgs/icons/left-circle.svg +3 -0
- package/_example/client/imgs/icons/left.svg +3 -0
- package/_example/client/imgs/icons/line-options.svg +5 -0
- package/_example/client/imgs/icons/line.svg +3 -0
- package/_example/client/imgs/icons/person.svg +7 -0
- package/_example/client/imgs/icons/plus-circle.svg +5 -0
- package/_example/client/imgs/icons/plus.svg +5 -0
- package/_example/client/imgs/icons/right-circle.svg +3 -0
- package/_example/client/imgs/icons/right.svg +3 -0
- package/_example/client/imgs/icons/search.svg +3 -0
- package/_example/client/imgs/icons/shield.svg +6 -0
- package/_example/client/imgs/icons/tick-circle-solid.svg +8 -0
- package/_example/client/imgs/icons/tick-circle.svg +6 -0
- package/_example/client/imgs/icons/tick.svg +5 -0
- package/_example/client/imgs/icons/up2-small.svg +4 -0
- package/_example/client/imgs/icons/up2.svg +4 -0
- package/_example/client/imgs/icons/updown.svg +6 -0
- package/_example/client/imgs/icons/v-big-dark.svg +3 -0
- package/_example/client/imgs/icons/v-dark.svg +3 -0
- package/_example/client/imgs/icons/v.svg +3 -0
- package/_example/client/imgs/icons/v2-active.svg +6 -0
- package/_example/client/imgs/icons/x1.svg +4 -0
- package/_example/client/imgs/logo/logo-white.svg +20 -0
- package/_example/client/imgs/logo/logo.svg +20 -0
- package/_example/client/imgs/no-image.jpg +0 -0
- package/_example/client/imgs/user.jpg +0 -0
- package/_example/client/index.html +12 -0
- package/_example/client/index.ts +47 -0
- package/_example/components/auth.api.js +1 -0
- package/_example/components/index.tsx +225 -0
- package/_example/components/partials/layouts.tsx +5 -0
- package/_example/components/settings.api.js +1 -0
- package/_example/server/config.js +120 -0
- package/_example/server/email/welcome.html +27 -0
- package/_example/server/index.js +32 -0
- package/_example/tailwind.config.js +84 -0
- package/_example/tsconfig.json +32 -0
- package/_example/types.d.ts +7 -0
- package/_example/webpack.config.js +4 -0
- package/client/app.js +300 -0
- package/client/css/components.css +84 -0
- package/client/css/fonts.css +67 -0
- package/client/imgs/icons/calendar.svg +3 -0
- package/client/imgs/icons/email.svg +6 -0
- package/client/imgs/icons/eye-open.svg +4 -0
- package/client/imgs/icons/eye.svg +5 -0
- package/client/imgs/icons/filter.svg +7 -0
- package/client/imgs/icons/left-circle.svg +3 -0
- package/client/imgs/icons/left.svg +3 -0
- package/client/imgs/icons/line-options.svg +5 -0
- package/client/imgs/icons/line.svg +3 -0
- package/client/imgs/icons/person.svg +7 -0
- package/client/imgs/icons/plus-circle.svg +5 -0
- package/client/imgs/icons/plus.svg +5 -0
- package/client/imgs/icons/right-circle.svg +3 -0
- package/client/imgs/icons/right.svg +3 -0
- package/client/imgs/icons/search.svg +3 -0
- package/client/imgs/icons/shield.svg +6 -0
- package/client/imgs/icons/tick-circle-solid.svg +8 -0
- package/client/imgs/icons/tick-circle.svg +6 -0
- package/client/imgs/icons/tick.svg +5 -0
- package/client/imgs/icons/up2-small.svg +4 -0
- package/client/imgs/icons/up2.svg +4 -0
- package/client/imgs/icons/updown.svg +6 -0
- package/client/imgs/icons/v-big-dark.svg +3 -0
- package/client/imgs/icons/v-dark.svg +3 -0
- package/client/imgs/icons/v.svg +3 -0
- package/client/imgs/icons/v2-active.svg +6 -0
- package/client/imgs/icons/x1.svg +4 -0
- package/client.js +42 -0
- package/components/auth/auth.api.js +419 -0
- package/components/auth/reset.jsx +88 -0
- package/components/auth/signin.jsx +74 -0
- package/components/auth/signup.jsx +62 -0
- package/components/billing/stripe.api.js +267 -0
- package/components/partials/element/accordion.jsx +82 -0
- package/components/partials/element/avatar.jsx +28 -0
- package/components/partials/element/button.jsx +66 -0
- package/components/partials/element/dropdown.jsx +185 -0
- package/components/partials/element/initials.jsx +56 -0
- package/components/partials/element/message.jsx +124 -0
- package/components/partials/element/modal.jsx +229 -0
- package/components/partials/element/sidebar.jsx +166 -0
- package/components/partials/element/tooltip.jsx +146 -0
- package/components/partials/element/topbar.jsx +25 -0
- package/components/partials/form/checkbox.jsx +74 -0
- package/components/partials/form/drop-handler.jsx +62 -0
- package/components/partials/form/drop.jsx +125 -0
- package/components/partials/form/form-error.jsx +21 -0
- package/components/partials/form/input-color.jsx +77 -0
- package/components/partials/form/input-currency.jsx +133 -0
- package/components/partials/form/input-date.jsx +223 -0
- package/components/partials/form/input.jsx +131 -0
- package/components/partials/form/location.jsx +212 -0
- package/components/partials/form/select.jsx +369 -0
- package/components/partials/form/toggle.jsx +46 -0
- package/components/partials/is-first-render.js +15 -0
- package/components/partials/layout/layout1.jsx +32 -0
- package/components/partials/layout/layout2.jsx +47 -0
- package/components/partials/not-found.jsx +7 -0
- package/components/partials/styleguide.jsx +252 -0
- package/components/settings/settings-account.jsx +143 -0
- package/components/settings/settings-business.jsx +121 -0
- package/components/settings/settings-team--member.jsx +108 -0
- package/components/settings/settings-team.jsx +76 -0
- package/components/settings/settings.api.js +54 -0
- package/package.json +175 -0
- package/readme.md +43 -0
- package/server/email/index.js +192 -0
- package/server/email/partials/email.css +153 -0
- package/server/email/partials/layout1.swig +92 -0
- package/server/email/partials/line.swig +8 -0
- package/server/email/partials/vert-10.swig +8 -0
- package/server/email/partials/vert-15.swig +8 -0
- package/server/email/partials/vert-20.swig +8 -0
- package/server/email/partials/vert-25.swig +8 -0
- package/server/email/partials/vert-30.swig +8 -0
- package/server/email/partials/vert-35.swig +8 -0
- package/server/email/partials/vert-50.swig +8 -0
- package/server/email/reset-password.html +21 -0
- package/server/email/welcome.html +21 -0
- package/server/models/company.js +76 -0
- package/server/models/user.js +45 -0
- package/server/router.js +355 -0
- package/server.js +20 -0
- package/util.js +1145 -0
- 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
|
+
[](https://www.npmjs.com/package/nitro-web) [](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
|
+
}
|