nitro-web 0.0.32 → 0.0.34
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/client/index.ts +0 -1
- package/components/auth/auth.api.js +16 -12
- package/components/partials/element/button.tsx +4 -4
- package/components/partials/element/calendar.tsx +6 -6
- package/components/partials/element/sidebar.tsx +1 -0
- package/components/partials/element/topbar.tsx +5 -17
- package/components/partials/form/checkbox.tsx +16 -2
- package/components/partials/form/field-date.tsx +1 -1
- package/components/partials/form/field.tsx +3 -1
- package/components/partials/form/select.tsx +1 -1
- package/package.json +1 -1
- package/server/email/index.js +24 -18
- package/server/email/partials/layout1.swig +1 -1
- package/server/email/partials/vert-25.swig +1 -1
- package/server/email/reset-password.html +10 -2
- package/server/email/welcome.html +12 -3
- package/components/partials/form/toggle.tsx +0 -54
package/client/index.ts
CHANGED
|
@@ -48,7 +48,6 @@ export { FieldCurrency, type FieldCurrencyProps } from '../components/partials/f
|
|
|
48
48
|
export { FieldDate, type FieldDateProps } from '../components/partials/form/field-date'
|
|
49
49
|
export { Location } from '../components/partials/form/location'
|
|
50
50
|
export { Select, getSelectStyle } from '../components/partials/form/select'
|
|
51
|
-
export { Toggle } from '../components/partials/form/toggle'
|
|
52
51
|
|
|
53
52
|
// Component Other
|
|
54
53
|
export { IsFirstRender } from '../components/partials/is-first-render'
|
|
@@ -6,7 +6,7 @@ import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt'
|
|
|
6
6
|
import db from 'monastery'
|
|
7
7
|
import jsonwebtoken from 'jsonwebtoken'
|
|
8
8
|
import { sendEmail } from 'nitro-web/server'
|
|
9
|
-
import
|
|
9
|
+
import { isArray, pick, isString, ucFirst, fullNameSplit } from 'nitro-web/util'
|
|
10
10
|
|
|
11
11
|
let config = {}
|
|
12
12
|
const JWT_SECRET = process.env.JWT_SECRET || 'replace_this_with_secure_env_secret'
|
|
@@ -32,9 +32,13 @@ export default {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
function setup(middleware, _config) {
|
|
35
|
+
// Setup is called automatically when the server starts
|
|
35
36
|
// Set config values
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
const configKeys = ['clientUrl', 'emailFrom', 'env', 'mailgunDomain', 'mailgunKey', 'masterPassword', 'name']
|
|
38
|
+
config = pick(_config, configKeys)
|
|
39
|
+
for (const key of configKeys) {
|
|
40
|
+
if (!config[key]) throw new Error(`Missing config value for: config.${key}`)
|
|
41
|
+
}
|
|
38
42
|
|
|
39
43
|
passport.use(
|
|
40
44
|
new passportLocal.Strategy(
|
|
@@ -102,7 +106,7 @@ async function signup(req, res) {
|
|
|
102
106
|
sendEmail({
|
|
103
107
|
config: config,
|
|
104
108
|
template: 'welcome',
|
|
105
|
-
to: `${
|
|
109
|
+
to: `${ucFirst(user.firstName)}<${user.email}>`,
|
|
106
110
|
}).catch(console.error)
|
|
107
111
|
res.send(await signinAndGetState(user, req.query.desktop))
|
|
108
112
|
} catch (err) {
|
|
@@ -133,7 +137,7 @@ function signout(req, res) {
|
|
|
133
137
|
async function resetInstructions(req, res) {
|
|
134
138
|
try {
|
|
135
139
|
let email = (req.body.email || '').trim().toLowerCase()
|
|
136
|
-
if (!email || !
|
|
140
|
+
if (!email || !isString(email)) throw { title: 'email', detail: 'The email you entered is incorrect.' }
|
|
137
141
|
|
|
138
142
|
let user = await db.user.findOne({ query: { email }, _privateData: true })
|
|
139
143
|
if (!user) throw { title: 'email', detail: 'The email you entered is incorrect.' }
|
|
@@ -145,7 +149,7 @@ async function resetInstructions(req, res) {
|
|
|
145
149
|
sendEmail({
|
|
146
150
|
config: config,
|
|
147
151
|
template: 'reset-password',
|
|
148
|
-
to: `${
|
|
152
|
+
to: `${ucFirst(user.firstName)}<${email}>`,
|
|
149
153
|
data: {
|
|
150
154
|
token: resetToken + (req.query.hasOwnProperty('desktop') ? '?desktop' : ''),
|
|
151
155
|
},
|
|
@@ -271,21 +275,21 @@ async function validatePassword(password='', password2) {
|
|
|
271
275
|
}
|
|
272
276
|
}
|
|
273
277
|
|
|
274
|
-
export async function userCreate({ name,
|
|
278
|
+
export async function userCreate({ name, business, email, password }) {
|
|
275
279
|
try {
|
|
276
280
|
const options = { blacklist: ['-_id'] }
|
|
277
281
|
const userId = db.id()
|
|
278
282
|
const companyData = {
|
|
279
283
|
_id: db.id(),
|
|
280
|
-
business
|
|
284
|
+
...(business ? { business } : {}),
|
|
281
285
|
users: [{ _id: userId, role: 'owner', status: 'active' }],
|
|
282
286
|
}
|
|
283
287
|
const userData = {
|
|
284
288
|
_id: userId,
|
|
285
289
|
company: companyData._id,
|
|
286
290
|
email: email,
|
|
287
|
-
firstName:
|
|
288
|
-
lastName:
|
|
291
|
+
firstName: fullNameSplit(name)[0],
|
|
292
|
+
lastName: fullNameSplit(name)[1],
|
|
289
293
|
password: password ? await (await import('bcrypt')).hash(password, 10) : undefined,
|
|
290
294
|
}
|
|
291
295
|
|
|
@@ -298,7 +302,7 @@ export async function userCreate({ name, businessName, email, password }) {
|
|
|
298
302
|
|
|
299
303
|
// Throw all the errors from at once
|
|
300
304
|
const errors = results.filter(o => o.status == 'rejected').reduce((acc, o) => {
|
|
301
|
-
if (
|
|
305
|
+
if (isArray(o.reason)) acc.push(...o.reason)
|
|
302
306
|
else throw o.reason
|
|
303
307
|
return acc
|
|
304
308
|
}, [])
|
|
@@ -312,7 +316,7 @@ export async function userCreate({ name, businessName, email, password }) {
|
|
|
312
316
|
return await findUserFromProvider({ _id: userId })
|
|
313
317
|
|
|
314
318
|
} catch (err) {
|
|
315
|
-
if (!
|
|
319
|
+
if (!isArray(err)) throw err
|
|
316
320
|
throw err.map((o) => {
|
|
317
321
|
if (o.title == 'firstName') o.title = 'name'
|
|
318
322
|
return o
|
|
@@ -29,16 +29,16 @@ export function Button({
|
|
|
29
29
|
const iconPosition = IconLeft ? 'left' : IconLeftEnd ? 'leftEnd' : IconRight ? 'right' : IconRightEnd ? 'rightEnd' : 'none'
|
|
30
30
|
const base =
|
|
31
31
|
'relative inline-block text-center font-medium shadow-sm focus-visible:outline focus-visible:outline-2 ' +
|
|
32
|
-
'focus-visible:outline-offset-2 text-white'
|
|
32
|
+
'focus-visible:outline-offset-2 text-white [&>.loader]:border-white'
|
|
33
33
|
|
|
34
34
|
// Button colors, you can use custom colors by using className instead
|
|
35
35
|
const colors = {
|
|
36
36
|
primary: 'bg-primary hover:bg-primary-hover',
|
|
37
37
|
secondary: 'bg-secondary hover:bg-secondary-hover',
|
|
38
38
|
black: 'bg-black hover:bg-gray-700',
|
|
39
|
-
white: 'bg-white
|
|
39
|
+
white: 'bg-white ring-1 ring-inset ring-gray-300 hover:bg-gray-50 text-gray-900 [&>.loader]:border-black',
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
// Button sizes
|
|
43
43
|
const sizes = {
|
|
44
44
|
xs: 'px-2 py-1 px-button-x-xs py-button-y-xs text-xs rounded',
|
|
@@ -67,7 +67,7 @@ export function Button({
|
|
|
67
67
|
isLoading &&
|
|
68
68
|
<span className={
|
|
69
69
|
'loader !opacity-100 absolute top-[50%] left-[50%] w-[1rem] h-[1rem] ml-[-0.5rem] mt-[-0.5rem] ' +
|
|
70
|
-
'rounded-full animate-spin border-2 !border-t-transparent
|
|
70
|
+
'rounded-full animate-spin border-2 !border-t-transparent'
|
|
71
71
|
} />
|
|
72
72
|
}
|
|
73
73
|
</button>
|
|
@@ -87,8 +87,8 @@ export function Calendar({ mode='single', onChange, value, numberOfMonths, month
|
|
|
87
87
|
months: `${d.months} flex-nowrap`,
|
|
88
88
|
month_caption: `${d.month_caption} text-2xs pl-2`,
|
|
89
89
|
caption_label: `${d.caption_label} z-auto`,
|
|
90
|
-
button_previous: `${d.button_previous} size-8`,// [&:hover>svg]:fill-
|
|
91
|
-
button_next: `${d.button_next} size-8`,// [&:hover>svg]:fill-
|
|
90
|
+
button_previous: `${d.button_previous} size-8`,// [&:hover>svg]:fill-input-border-focus`,
|
|
91
|
+
button_next: `${d.button_next} size-8`,// [&:hover>svg]:fill-input-border-focus`,
|
|
92
92
|
chevron: `${d.chevron} fill-black size-[18px]`,
|
|
93
93
|
|
|
94
94
|
// Days
|
|
@@ -98,12 +98,12 @@ export function Calendar({ mode='single', onChange, value, numberOfMonths, month
|
|
|
98
98
|
|
|
99
99
|
// States
|
|
100
100
|
focused: `${d.focused} [&>button]:bg-gray-200 [&>button]:border-gray-200`,
|
|
101
|
-
range_start: `${d.range_start} [&>button]:!bg-
|
|
102
|
-
range_end: `${d.range_end} [&>button]:!bg-
|
|
101
|
+
range_start: `${d.range_start} [&>button]:!bg-input-border-focus [&>button]:!border-input-border-focus`,
|
|
102
|
+
range_end: `${d.range_end} [&>button]:!bg-input-border-focus [&>button]:!border-input-border-focus`,
|
|
103
103
|
selected: `${d.selected} font-normal `
|
|
104
104
|
+ '[&:not(.rangemiddle)>button]:!text-white '
|
|
105
|
-
+ '[&:not(.rangemiddle)>button]:!bg-
|
|
106
|
-
+ '[&:not(.rangemiddle)>button]:!border-
|
|
105
|
+
+ '[&:not(.rangemiddle)>button]:!bg-input-border-focus '
|
|
106
|
+
+ '[&:not(.rangemiddle)>button]:!border-input-border-focus ',
|
|
107
107
|
},
|
|
108
108
|
}
|
|
109
109
|
|
|
@@ -1,27 +1,15 @@
|
|
|
1
1
|
type TopbarProps = {
|
|
2
2
|
title: React.ReactNode
|
|
3
3
|
subtitle?: React.ReactNode
|
|
4
|
-
submenu?: React.ReactNode
|
|
5
|
-
btns?: React.ReactNode
|
|
6
4
|
className?: string
|
|
7
5
|
}
|
|
8
6
|
|
|
9
|
-
export function Topbar({ title, subtitle,
|
|
7
|
+
export function Topbar({ title, subtitle, className }: TopbarProps) {
|
|
10
8
|
return (
|
|
11
|
-
<div class={`flex
|
|
12
|
-
<div class="
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
<h1 class="h1 mb-0">{title}</h1>
|
|
16
|
-
</div>
|
|
17
|
-
{
|
|
18
|
-
submenu &&
|
|
19
|
-
<div class="pt-2 text-large weight-500">{submenu}</div>
|
|
20
|
-
}
|
|
21
|
-
</div>
|
|
22
|
-
<div class="">
|
|
23
|
-
{btns}
|
|
24
|
-
</div>
|
|
9
|
+
<div class={`flex flex-col min-h-12 gap-0.5 mb-6 nitro-topbar ${className||''}`}>
|
|
10
|
+
<div class="text-2xl font-bold">{title}</div>
|
|
11
|
+
{ subtitle && <div class="text-sm text-muted-foreground">{subtitle}</div>}
|
|
12
|
+
{/* { submenu && <div class="pt-2 text-large weight-500">{submenu}</div> } */}
|
|
25
13
|
</div>
|
|
26
14
|
)
|
|
27
15
|
}
|
|
@@ -42,7 +42,13 @@ export function Checkbox({ name, id, size='sm', subtext, text, type='checkbox',
|
|
|
42
42
|
id={id}
|
|
43
43
|
name={name}
|
|
44
44
|
type={type}
|
|
45
|
-
className={
|
|
45
|
+
className={
|
|
46
|
+
`${type === 'radio' ? 'rounded-full' : 'rounded'} col-start-1 row-start-1 appearance-none border border-gray-300 bg-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 disabled:border-gray-300 disabled:bg-gray-100 disabled:checked:bg-gray-100 forced-colors:appearance-auto ` +
|
|
47
|
+
// Default
|
|
48
|
+
'checked:border-blue-600 checked:bg-blue-600 indeterminate:border-blue-600 indeterminate:bg-blue-600 focus-visible:outline-blue-600 ' +
|
|
49
|
+
// Variable-selected color defined?
|
|
50
|
+
'checked:!border-variable-selected checked:!bg-variable-selected indeterminate:!border-variable-selected indeterminate:!bg-variable-selected focus-visible:!outline-variable-selected'
|
|
51
|
+
}
|
|
46
52
|
/>
|
|
47
53
|
<svg
|
|
48
54
|
fill="none"
|
|
@@ -89,7 +95,15 @@ export function Checkbox({ name, id, size='sm', subtext, text, type='checkbox',
|
|
|
89
95
|
/>
|
|
90
96
|
<label
|
|
91
97
|
for={id}
|
|
92
|
-
className={
|
|
98
|
+
className={
|
|
99
|
+
`col-start-1 row-start-1 relative ${_size.toggleWidth} ${_size.toggleHeight} bg-gray-200 peer-focus-visible:outline-none peer-focus-visible:ring-4 rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full ${_size.toggleAfterSize} after:transition-all ` +
|
|
100
|
+
// Default
|
|
101
|
+
'peer-focus-visible:ring-blue-300 peer-checked:bg-blue-600 ' +
|
|
102
|
+
// Variable-selected color defined?
|
|
103
|
+
'peer-focus-visible:!ring-variable-selected peer-checked:!bg-variable-selected '
|
|
104
|
+
// Dark mode not used yet...
|
|
105
|
+
// 'dark:peer-focus-visible:ring-blue-800 dark:bg-gray-700 dark:border-gray-600 '
|
|
106
|
+
}
|
|
93
107
|
/>
|
|
94
108
|
</div>
|
|
95
109
|
}
|
|
@@ -179,7 +179,7 @@ function TimePicker({ date, onChange }: TimePickerProps) {
|
|
|
179
179
|
key={item}
|
|
180
180
|
className={
|
|
181
181
|
'size-[33px] rounded-full flex justify-center items-center group-hover:bg-gray-100 '
|
|
182
|
-
+ (item === currentValue ? '!bg-
|
|
182
|
+
+ (item === currentValue ? '!bg-input-border-focus text-white' : '')
|
|
183
183
|
}
|
|
184
184
|
onClick={() => handleTimeChange(type, item)}
|
|
185
185
|
>
|
|
@@ -143,7 +143,9 @@ function getInputClasses({ error, Icon, iconPos, type }: { error: Error, Icon?:
|
|
|
143
143
|
`block ${py} col-start-1 row-start-1 w-full rounded-md bg-white text-sm leading-[1.65] outline outline-1 -outline-offset-1 ` +
|
|
144
144
|
'placeholder:text-input-placeholder focus:outline focus:outline-2 focus:-outline-offset-2 ' +
|
|
145
145
|
(iconPos == 'right' && Icon ? `${pl} ${prWithIcon} ` : (Icon ? `${plWithIcon} ${pr} ` : `${pl} ${pr} `)) +
|
|
146
|
-
(error
|
|
146
|
+
(error
|
|
147
|
+
? 'text-red-900 outline-danger focus:outline-danger '
|
|
148
|
+
: 'text-input outline-input-border focus:outline-input-border-focus ') +
|
|
147
149
|
(iconPos == 'right' ? 'justify-self-start ' : 'justify-self-end ') +
|
|
148
150
|
'nitro-input'
|
|
149
151
|
)
|
|
@@ -238,7 +238,7 @@ const selectStyles = {
|
|
|
238
238
|
// Input container
|
|
239
239
|
control: {
|
|
240
240
|
base: 'rounded-md bg-white hover:cursor-pointer text-sm leading-[1.65] outline outline-1 -outline-offset-1 outline-input-border',
|
|
241
|
-
focus: 'outline-2 -outline-offset-2 outline-
|
|
241
|
+
focus: 'outline-2 -outline-offset-2 outline-input-border-focus',
|
|
242
242
|
error: 'outline-danger',
|
|
243
243
|
},
|
|
244
244
|
valueContainer: 'py-2 px-3 py-input-y px-input-x gap-1',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nitro-web",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.34",
|
|
4
4
|
"repository": "github:boycce/nitro-web",
|
|
5
5
|
"homepage": "https://boycce.github.io/nitro-web/",
|
|
6
6
|
"description": "Nitro is a battle-tested, modular base project to turbocharge your projects, styled using Tailwind 🚀",
|
package/server/email/index.js
CHANGED
|
@@ -7,21 +7,21 @@ import inlineCss from 'inline-css'
|
|
|
7
7
|
import { dirname, join } from 'path'
|
|
8
8
|
import { fileURLToPath } from 'url'
|
|
9
9
|
import path from 'path'
|
|
10
|
-
import { getDirectories } from 'nitro-web/util'
|
|
10
|
+
import { getDirectories, ucFirst } from 'nitro-web/util'
|
|
11
11
|
|
|
12
12
|
let templates = {}
|
|
13
13
|
let nodemailerMailgun = undefined
|
|
14
14
|
const _dirname = dirname(fileURLToPath(import.meta.url)) + '/'
|
|
15
15
|
|
|
16
|
-
export async function sendEmail({ template, to, bcc, data
|
|
16
|
+
export async function sendEmail({ template, to, bcc, data, from, replyTo, recipientVariables, subject, test, skipCssInline, config }) {
|
|
17
17
|
/**
|
|
18
18
|
* Email recipient a predefined template with data and/or recipientVariables
|
|
19
19
|
*
|
|
20
20
|
* @param {string} template = e.g. 'reset-password' or html
|
|
21
|
-
* @param {string} to - e.g. "Bruce
|
|
22
|
-
* @param {object} config - e.g. { mailgunKey, mailgunDomain, emailFrom, clientUrl }
|
|
21
|
+
* @param {string} to - e.g. "Bruce Wayne<bruce@wayneenterprises.com>, ..."
|
|
22
|
+
* @param {object} config - e.g. { mailgunKey, mailgunDomain, emailFrom, clientUrl, name }
|
|
23
23
|
* @param {string} <bcc> - e.g. "Chuck Norris<chuck@gmail.com>" (not sent in development)
|
|
24
|
-
* @param {object} <data> - recipientVariables[to] shorthand
|
|
24
|
+
* @param {object} <data> - recipientVariables[to] shorthand, data is copied to all recipients
|
|
25
25
|
* @param {string} <from> - e.g. "Chuck Norris<chuck@gmail.com>"
|
|
26
26
|
* @param {string} <replyTo> - e.g. "Chuck Norris<chuck@gmail.com>"
|
|
27
27
|
* @param {object} <recipientVariables> - mailgun recipient-variables for batch sending
|
|
@@ -32,8 +32,12 @@ export async function sendEmail({ template, to, bcc, data={}, from, replyTo, rec
|
|
|
32
32
|
*/
|
|
33
33
|
if (!config) {
|
|
34
34
|
throw new Error('sendEmail: `config` missing')
|
|
35
|
-
} else if (!config.emailFrom
|
|
36
|
-
throw new Error('sendEmail: `config.emailFrom`
|
|
35
|
+
} else if (!config.emailFrom) {
|
|
36
|
+
throw new Error('sendEmail: `config.emailFrom` is missing')
|
|
37
|
+
} else if (!config.clientUrl) {
|
|
38
|
+
throw new Error('sendEmail: `config.clientUrl` is missing')
|
|
39
|
+
} else if (!config.name) {
|
|
40
|
+
throw new Error('sendEmail: `config.name` is missing')
|
|
37
41
|
} else if (!test && (!config.mailgunKey || !config.mailgunDomain)) {
|
|
38
42
|
throw new Error('sendEmail: `config.mailgunKey` or `config.mailgunDomain` is missing')
|
|
39
43
|
} else if (!template) {
|
|
@@ -52,27 +56,30 @@ export async function sendEmail({ template, to, bcc, data={}, from, replyTo, rec
|
|
|
52
56
|
// From, replayTo
|
|
53
57
|
from = from || config.emailFrom
|
|
54
58
|
replyTo = replyTo || config.emailReplyTo || from
|
|
55
|
-
|
|
56
|
-
// Data is recipientVariables[to] shorthand
|
|
57
|
-
if (data) {
|
|
58
|
-
recipientVariables = { [getNameEmail(to)[1]]: data }
|
|
59
|
-
}
|
|
59
|
+
const toSplit = to.split(',')
|
|
60
60
|
|
|
61
61
|
// Add default recipientVariables
|
|
62
|
-
|
|
62
|
+
if (!recipientVariables) recipientVariables = {}
|
|
63
|
+
|
|
64
|
+
// Add default values to recipientVariables
|
|
65
|
+
for (let toNameEmail of toSplit) {
|
|
66
|
+
const [toName, toEmail] = getNameEmail(toNameEmail)
|
|
63
67
|
recipientVariables[toEmail] = {
|
|
68
|
+
...(data || {}),
|
|
69
|
+
configName: ucFirst(config.name),
|
|
64
70
|
domain: config.clientUrl,
|
|
65
|
-
email: toEmail,
|
|
66
|
-
greet: data.name || getNameEmail(to)[0]? 'Hi ' + (data.name || getNameEmail(to)[0]) : 'Hello',
|
|
67
|
-
name: getNameEmail(to)[0],
|
|
68
71
|
replyToEmail: getNameEmail(replyTo)[1],
|
|
69
72
|
replyToName: getNameEmail(replyTo)[0],
|
|
70
|
-
|
|
73
|
+
email: toEmail,
|
|
74
|
+
greet: toName ? 'Hi ' + toName : 'Hello',
|
|
75
|
+
name: toName,
|
|
76
|
+
...(recipientVariables[toEmail] || {}),
|
|
71
77
|
}
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
let settings = {
|
|
75
81
|
bcc: bcc,
|
|
82
|
+
emailTemplateDir: getDirectories(path, config.pwd).emailTemplateDir,
|
|
76
83
|
from: from,
|
|
77
84
|
isDev: config.clientUrl.match(/:/), // possibly use config.env here
|
|
78
85
|
recipientVariables: recipientVariables,
|
|
@@ -83,7 +90,6 @@ export async function sendEmail({ template, to, bcc, data={}, from, replyTo, rec
|
|
|
83
90
|
test: config.emailTestMode || test,
|
|
84
91
|
to: to,
|
|
85
92
|
url: config.clientUrl,
|
|
86
|
-
emailTemplateDir: getDirectories(path, config.pwd).emailTemplateDir,
|
|
87
93
|
}
|
|
88
94
|
|
|
89
95
|
// Grab html and send
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
<tr>
|
|
32
32
|
<td width="180px" align="left">
|
|
33
33
|
<a href="%recipient.domain%" target="_blank">
|
|
34
|
-
<img src="%recipient.domain%/assets/imgs/logo/logo
|
|
34
|
+
<img src="%recipient.domain%/assets/imgs/logo/logo.svg" width="auto" height="22" border="0">
|
|
35
35
|
</a>
|
|
36
36
|
</td>
|
|
37
37
|
<td align="right">
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
- You can view templates at http://localhost:3001/email/reset-password
|
|
3
|
+
- To modify the css, add your own /server/email/partials/email.css file
|
|
4
|
+
- You can copy any nitro email .swig/html template to your project, and modify it
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
<!-- extends references the default nitro layout file -->
|
|
1
8
|
{% extends "partials/layout1.swig" %}
|
|
9
|
+
|
|
10
|
+
<!-- block content is the content of the email -->
|
|
2
11
|
{% block content %}
|
|
3
12
|
|
|
4
13
|
[[ subject = Reset your Password ]]
|
|
@@ -15,7 +24,6 @@
|
|
|
15
24
|
</span><br/>
|
|
16
25
|
<br/>
|
|
17
26
|
Thanks,<br/>
|
|
18
|
-
<b>The
|
|
19
|
-
|
|
27
|
+
<b>The %recipient.configName% Team</b>
|
|
20
28
|
|
|
21
29
|
{% endblock %}
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
- You can view templates at http://localhost:3001/email/welcome
|
|
3
|
+
- To modify the css, add your own /server/email/partials/email.css file
|
|
4
|
+
- You can copy any nitro email .swig/html template to your project, and modify it
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
<!-- extends references the default nitro layout file -->
|
|
1
8
|
{% extends "partials/layout1.swig" %}
|
|
9
|
+
|
|
10
|
+
<!-- block content is the content of the email -->
|
|
2
11
|
{% block content %}
|
|
3
12
|
|
|
4
|
-
[[ subject = Welcome to
|
|
13
|
+
[[ subject = Welcome to %recipient.configName% ]]
|
|
5
14
|
<b>%recipient.greet%</b>,<br/>
|
|
6
15
|
<br/>
|
|
7
|
-
Thanks for trying out
|
|
16
|
+
Thanks for trying out %recipient.configName%! <br/>
|
|
8
17
|
<br/>
|
|
9
18
|
If you have any feature requests, feedback or questions, please don't hesitate to reach out to us at <a href="mailto:%recipient.replyToEmail%">%recipient.replyToEmail%</a>.<br/>
|
|
10
19
|
<br/>
|
|
@@ -16,6 +25,6 @@
|
|
|
16
25
|
</span><br/>
|
|
17
26
|
<br/>
|
|
18
27
|
Thanks,<br/>
|
|
19
|
-
<b>The
|
|
28
|
+
<b>The %recipient.configName% Team</b>
|
|
20
29
|
|
|
21
30
|
{% endblock %}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
type ToggleProps = {
|
|
2
|
-
className?: string
|
|
3
|
-
id?: string
|
|
4
|
-
name: string
|
|
5
|
-
subtext?: string
|
|
6
|
-
text?: string
|
|
7
|
-
type?: 'checkbox'
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function Toggle({ name, id, subtext, text, type='checkbox', ...props }: ToggleProps) {
|
|
11
|
-
id = id || name
|
|
12
|
-
// https://tailwindui.com/components/application-ui/forms/checkboxes#component-744ed4fa65ba36b925701eb4da5c6e31
|
|
13
|
-
return (
|
|
14
|
-
<div className={`mt-2.5 mb-6 mt-input-before mb-input-after flex gap-3 nitro-toggle ${props.className || ''}`}>
|
|
15
|
-
<div className="flex h-6 shrink-0 items-center">
|
|
16
|
-
<div className="group grid size-4 grid-cols-1">
|
|
17
|
-
<input
|
|
18
|
-
{...props}
|
|
19
|
-
id={id}
|
|
20
|
-
name={name}
|
|
21
|
-
type={type}
|
|
22
|
-
className="col-start-1 row-start-1 appearance-none rounded border border-gray-300 bg-white checked:border-indigo-600 checked:bg-indigo-600 indeterminate:border-indigo-600 indeterminate:bg-indigo-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:border-gray-300 disabled:bg-gray-100 disabled:checked:bg-gray-100 forced-colors:appearance-auto"
|
|
23
|
-
/>
|
|
24
|
-
<svg
|
|
25
|
-
fill="none"
|
|
26
|
-
viewBox="0 0 14 14"
|
|
27
|
-
className="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-gray-950/25"
|
|
28
|
-
>
|
|
29
|
-
<path
|
|
30
|
-
d="M3 8L6 11L11 3.5"
|
|
31
|
-
strokeWidth={2}
|
|
32
|
-
strokeLinecap="round"
|
|
33
|
-
strokeLinejoin="round"
|
|
34
|
-
className="opacity-0 group-has-[:checked]:opacity-100"
|
|
35
|
-
/>
|
|
36
|
-
<path
|
|
37
|
-
d="M3 7H11"
|
|
38
|
-
strokeWidth={2}
|
|
39
|
-
strokeLinecap="round"
|
|
40
|
-
strokeLinejoin="round"
|
|
41
|
-
className="opacity-0 group-has-[:indeterminate]:opacity-100"
|
|
42
|
-
/>
|
|
43
|
-
</svg>
|
|
44
|
-
</div>
|
|
45
|
-
</div>
|
|
46
|
-
{text && <div className="text-sm/6">
|
|
47
|
-
<label for={id}>
|
|
48
|
-
<span className="font-medium text-gray-900">{text}</span>
|
|
49
|
-
<span className="ml-2 text-gray-500">{subtext}</span>
|
|
50
|
-
</label>
|
|
51
|
-
</div>}
|
|
52
|
-
</div>
|
|
53
|
-
)
|
|
54
|
-
}
|