@firedrill-tools/resend 0.1.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/LICENSE +201 -0
- package/README.md +198 -0
- package/firedrill/agent.target.json +16 -0
- package/firedrill/baseline.scenario.json +1687 -0
- package/firedrill/conformance.suite.json +19 -0
- package/firedrill/contacts-outage.scenario.json +11 -0
- package/firedrill/quota-exhausted.scenario.json +17 -0
- package/firedrill/rate-limited.scenario.json +11 -0
- package/firedrill/resend-audience.drill.json +407 -0
- package/firedrill/resend-contacts-outage.drill.json +63 -0
- package/firedrill/resend-denied.drill.json +68 -0
- package/firedrill/resend-domains-and-keys.drill.json +307 -0
- package/firedrill/resend-email-flow.drill.json +286 -0
- package/firedrill/resend-fresh-install.drill.json +73 -0
- package/firedrill/resend-invalid-key.drill.json +803 -0
- package/firedrill/resend-large-page.drill.json +53 -0
- package/firedrill/resend-quota-exhausted.drill.json +82 -0
- package/firedrill/resend-rate-limited.drill.json +81 -0
- package/firedrill/resend-restricted-key.drill.json +441 -0
- package/firedrill/resend-tight-limits.drill.json +163 -0
- package/firedrill/tight-limits.scenario.json +16 -0
- package/firedrill/tools/resend/app/assets/ATTRIBUTION.md +30 -0
- package/firedrill/tools/resend/app/assets/fonts/Inter-OFL.txt +93 -0
- package/firedrill/tools/resend/app/assets/fonts/JetBrainsMono-OFL.txt +93 -0
- package/firedrill/tools/resend/app/assets/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/firedrill/tools/resend/app/assets/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/firedrill/tools/resend/app/assets/resend-icon-white.svg +3 -0
- package/firedrill/tools/resend/app/assets/resend-wordmark-white.svg +3 -0
- package/firedrill/tools/resend/app/assets/resend.svg +3 -0
- package/firedrill/tools/resend/app/site/app.js +90 -0
- package/firedrill/tools/resend/app/site/assets/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/firedrill/tools/resend/app/site/assets/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/firedrill/tools/resend/app/site/assets/resend-icon-white.svg +3 -0
- package/firedrill/tools/resend/app/site/assets/resend-wordmark-white.svg +3 -0
- package/firedrill/tools/resend/app/site/assets/resend.svg +3 -0
- package/firedrill/tools/resend/app/site/contact-panel.js +86 -0
- package/firedrill/tools/resend/app/site/icons.js +70 -0
- package/firedrill/tools/resend/app/site/index.html +56 -0
- package/firedrill/tools/resend/app/site/list.js +41 -0
- package/firedrill/tools/resend/app/site/styles.css +67 -0
- package/firedrill/tools/resend/app/site/ui.js +259 -0
- package/firedrill/tools/resend/app/site/view-audience.js +160 -0
- package/firedrill/tools/resend/app/site/view-domains.js +106 -0
- package/firedrill/tools/resend/app/site/view-email-detail.js +126 -0
- package/firedrill/tools/resend/app/site/view-emails.js +83 -0
- package/firedrill/tools/resend/app/site/view-keys.js +91 -0
- package/firedrill/tools/resend/app/site/views.css +141 -0
- package/firedrill/tools/resend/behavior.mjs +114 -0
- package/firedrill/tools/resend/lib/check.mjs +75 -0
- package/firedrill/tools/resend/lib/core.mjs +112 -0
- package/firedrill/tools/resend/lib/json-depth.mjs +45 -0
- package/firedrill/tools/resend/lib/page.mjs +66 -0
- package/firedrill/tools/resend/lib/time.mjs +60 -0
- package/firedrill/tools/resend/lib/wire.mjs +98 -0
- package/firedrill/tools/resend/ops/contacts.mjs +199 -0
- package/firedrill/tools/resend/ops/domains.mjs +125 -0
- package/firedrill/tools/resend/ops/email-model.mjs +100 -0
- package/firedrill/tools/resend/ops/emails.mjs +139 -0
- package/firedrill/tools/resend/ops/keys-segments.mjs +97 -0
- package/firedrill/tools/resend/resend.tool.json +4804 -0
- package/firedrill/world.json +2148 -0
- package/firedrill.json +5 -0
- package/package.json +62 -0
- package/starter.json +1686 -0
- package/test/conformance.mjs +480 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"id": "resend-tight-limits",
|
|
4
|
+
"title": "Scanning operations fail application_error instead of truncating and remove nothing",
|
|
5
|
+
"targetId": "conformance-agent",
|
|
6
|
+
"scenarioId": "tight-limits",
|
|
7
|
+
"actorId": "owner",
|
|
8
|
+
"task": {
|
|
9
|
+
"instruction": "Run the tight-limits flow against the synthetic Resend Tool and fail loudly on any unexpected status, header or body."
|
|
10
|
+
},
|
|
11
|
+
"assertions": [
|
|
12
|
+
{
|
|
13
|
+
"id": "emails-list-tool-error",
|
|
14
|
+
"kind": "operation.count",
|
|
15
|
+
"operation": {
|
|
16
|
+
"packageId": "resend",
|
|
17
|
+
"operationId": "emails.list"
|
|
18
|
+
},
|
|
19
|
+
"outcomes": [
|
|
20
|
+
"tool_error"
|
|
21
|
+
],
|
|
22
|
+
"comparison": {
|
|
23
|
+
"operator": "greater_than_or_equal",
|
|
24
|
+
"value": 1
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "domains-list-tool-error",
|
|
29
|
+
"kind": "operation.count",
|
|
30
|
+
"operation": {
|
|
31
|
+
"packageId": "resend",
|
|
32
|
+
"operationId": "domains.list"
|
|
33
|
+
},
|
|
34
|
+
"outcomes": [
|
|
35
|
+
"tool_error"
|
|
36
|
+
],
|
|
37
|
+
"comparison": {
|
|
38
|
+
"operator": "greater_than_or_equal",
|
|
39
|
+
"value": 1
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "api-keys-list-tool-error",
|
|
44
|
+
"kind": "operation.count",
|
|
45
|
+
"operation": {
|
|
46
|
+
"packageId": "resend",
|
|
47
|
+
"operationId": "api_keys.list"
|
|
48
|
+
},
|
|
49
|
+
"outcomes": [
|
|
50
|
+
"tool_error"
|
|
51
|
+
],
|
|
52
|
+
"comparison": {
|
|
53
|
+
"operator": "greater_than_or_equal",
|
|
54
|
+
"value": 1
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"id": "segments-list-tool-error",
|
|
59
|
+
"kind": "operation.count",
|
|
60
|
+
"operation": {
|
|
61
|
+
"packageId": "resend",
|
|
62
|
+
"operationId": "segments.list"
|
|
63
|
+
},
|
|
64
|
+
"outcomes": [
|
|
65
|
+
"tool_error"
|
|
66
|
+
],
|
|
67
|
+
"comparison": {
|
|
68
|
+
"operator": "greater_than_or_equal",
|
|
69
|
+
"value": 1
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"id": "contacts-list-tool-error",
|
|
74
|
+
"kind": "operation.count",
|
|
75
|
+
"operation": {
|
|
76
|
+
"packageId": "resend",
|
|
77
|
+
"operationId": "contacts.list"
|
|
78
|
+
},
|
|
79
|
+
"outcomes": [
|
|
80
|
+
"tool_error"
|
|
81
|
+
],
|
|
82
|
+
"comparison": {
|
|
83
|
+
"operator": "greater_than_or_equal",
|
|
84
|
+
"value": 1
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"id": "contacts-list-segments-tool-error",
|
|
89
|
+
"kind": "operation.count",
|
|
90
|
+
"operation": {
|
|
91
|
+
"packageId": "resend",
|
|
92
|
+
"operationId": "contacts.list_segments"
|
|
93
|
+
},
|
|
94
|
+
"outcomes": [
|
|
95
|
+
"tool_error"
|
|
96
|
+
],
|
|
97
|
+
"comparison": {
|
|
98
|
+
"operator": "greater_than_or_equal",
|
|
99
|
+
"value": 1
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"id": "segments-remove-tool-error",
|
|
104
|
+
"kind": "operation.count",
|
|
105
|
+
"operation": {
|
|
106
|
+
"packageId": "resend",
|
|
107
|
+
"operationId": "segments.remove"
|
|
108
|
+
},
|
|
109
|
+
"outcomes": [
|
|
110
|
+
"tool_error"
|
|
111
|
+
],
|
|
112
|
+
"comparison": {
|
|
113
|
+
"operator": "greater_than_or_equal",
|
|
114
|
+
"value": 1
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"id": "contacts-remove-tool-error",
|
|
119
|
+
"kind": "operation.count",
|
|
120
|
+
"operation": {
|
|
121
|
+
"packageId": "resend",
|
|
122
|
+
"operationId": "contacts.remove"
|
|
123
|
+
},
|
|
124
|
+
"outcomes": [
|
|
125
|
+
"tool_error"
|
|
126
|
+
],
|
|
127
|
+
"comparison": {
|
|
128
|
+
"operator": "greater_than_or_equal",
|
|
129
|
+
"value": 1
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"id": "segments-count",
|
|
134
|
+
"kind": "state.count",
|
|
135
|
+
"packageId": "resend",
|
|
136
|
+
"namespace": "segments",
|
|
137
|
+
"comparison": {
|
|
138
|
+
"operator": "equals",
|
|
139
|
+
"value": 3
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"id": "contacts-count",
|
|
144
|
+
"kind": "state.count",
|
|
145
|
+
"packageId": "resend",
|
|
146
|
+
"namespace": "contacts",
|
|
147
|
+
"comparison": {
|
|
148
|
+
"operator": "equals",
|
|
149
|
+
"value": 14
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"id": "segment-members-count",
|
|
154
|
+
"kind": "state.count",
|
|
155
|
+
"packageId": "resend",
|
|
156
|
+
"namespace": "segment-members",
|
|
157
|
+
"comparison": {
|
|
158
|
+
"operator": "equals",
|
|
159
|
+
"value": 14
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
]
|
|
163
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"id": "tight-limits",
|
|
4
|
+
"title": "Scan bound lowered to 1 row: scanning operations fail with application_error instead of truncating",
|
|
5
|
+
"state": [
|
|
6
|
+
{
|
|
7
|
+
"action": "upsert",
|
|
8
|
+
"packageId": "resend",
|
|
9
|
+
"namespace": "meta",
|
|
10
|
+
"rowId": "limits",
|
|
11
|
+
"value": {
|
|
12
|
+
"maxScanRows": 1
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Third-party assets used by the Resend Tool app
|
|
2
|
+
|
|
3
|
+
The browser app is served from `../site/`. This directory keeps the attribution record, the font licences and the
|
|
4
|
+
original downloads; the copies under `../site/assets/` are byte-identical (the framework serves only
|
|
5
|
+
html/css/js/json/image/font files, so Markdown and licence text cannot live inside the served root).
|
|
6
|
+
|
|
7
|
+
## Logos (trademarks of Resend, Inc.)
|
|
8
|
+
|
|
9
|
+
| file | served copy | source | notes |
|
|
10
|
+
|---|---|---|---|
|
|
11
|
+
| `resend.svg` | `../site/assets/resend.svg` | https://logos.lndev.me/logos/resend.svg (library: https://github.com/ln-dev7/logos-apps) | Current Resend "R" icon, black fill. The library has no wordmark (`resend-wordmark.svg` answers 404). Kept as the library reference copy. |
|
|
12
|
+
| `resend-icon-white.svg` | `../site/assets/resend-icon-white.svg` | https://cdn.resend.com/brand/resend-icon-white.svg (linked from https://resend.com/brand) | Same path data as the library icon with fill `#FDFDFD`, the variant the dark dashboard uses. Page favicon. |
|
|
13
|
+
| `resend-wordmark-white.svg` | `../site/assets/resend-wordmark-white.svg` | https://cdn.resend.com/brand/resend-wordmark-white.svg (linked from https://resend.com/brand) | Current "Resend" wordmark, white, shown at the top of the sidebar. |
|
|
14
|
+
|
|
15
|
+
Downloaded on 2026-09-15 and used unmodified.
|
|
16
|
+
|
|
17
|
+
## Fonts (SIL Open Font License 1.1)
|
|
18
|
+
|
|
19
|
+
| file | source | licence |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| `fonts/inter-latin-wght-normal.woff2` | https://cdn.jsdelivr.net/npm/@fontsource-variable/inter/files/inter-latin-wght-normal.woff2 (Inter by The Inter Project Authors, https://github.com/rsms/inter) | `fonts/Inter-OFL.txt` |
|
|
22
|
+
| `fonts/jetbrains-mono-latin-400-normal.woff2` | https://cdn.jsdelivr.net/npm/@fontsource/jetbrains-mono/files/jetbrains-mono-latin-400-normal.woff2 (JetBrains Mono by The JetBrains Mono Project Authors, https://github.com/JetBrains/JetBrainsMono) | `fonts/JetBrainsMono-OFL.txt` |
|
|
23
|
+
|
|
24
|
+
Inter matches the dashboard's UI typeface; JetBrains Mono stands in for its monospace face (ids, DNS values, tokens).
|
|
25
|
+
Only the Latin subsets are bundled; other scripts fall back to the system font stack.
|
|
26
|
+
|
|
27
|
+
## Icons
|
|
28
|
+
|
|
29
|
+
The line icons in `../site/icons.js` are drawn for this app in a generic thin-stroke style; they are not copied from
|
|
30
|
+
any icon set.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter) Inter-Italic[opsz,wght].ttf: Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter)
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
http://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) JetBrainsMono-Italic[wght].ttf: Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
http://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
Binary file
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="1800" height="1800" viewBox="0 0 1800 1800" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M1000.46 450C1174.77 450 1278.43 553.669 1278.43 691.282C1278.43 828.896 1174.77 932.563 1000.46 932.563H912.382L1350 1350H1040.82L707.794 1033.48C683.944 1011.47 672.936 985.781 672.935 963.765C672.935 932.572 694.959 905.049 737.161 893.122L908.712 847.244C973.85 829.812 1018.81 779.353 1018.81 713.298C1018.8 632.567 952.745 585.78 871.095 585.78H450V450H1000.46Z" fill="#FDFDFD"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="1978" height="420" viewBox="0 0 1978 420" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M543.803 118.321C633.178 118.321 698.215 180.419 693.6 293.286H475.948C484.076 324.654 505.791 351.608 549.257 351.608C573.594 351.608 595.833 344.895 612.617 323.916H687.726L686.467 330.629C673.459 388.112 608.001 420 549.257 420C456.106 420 393.585 357.062 393.585 269.37C393.585 181.678 456.106 118.321 543.803 118.321ZM1173.62 118.321C1263.42 118.321 1328.04 180.419 1323.84 293.286H1106.13C1114.16 324.654 1135.72 351.608 1179.5 351.608C1203.84 351.608 1226.07 344.895 1242.44 323.916H1317.97L1316.71 330.629C1303.7 388.111 1238.24 420 1179.5 420C1086.35 420 1023.83 357.063 1023.83 269.37C1023.83 181.678 1086.35 118.321 1173.62 118.321ZM1978 411.609H1896.6L1900.79 370.91C1889.46 395.246 1852.96 419.162 1807.22 419.162C1731.69 419.162 1670.43 361.26 1670.43 269.791C1670.43 178.322 1730.85 120.42 1807.22 120.42C1848.76 120.42 1874.36 133.847 1897.02 156.924V0H1978V411.609ZM853.47 116.644C919.767 116.644 978.091 140.559 994.875 199.301L996.554 205.595H913.892C896.268 183.777 872.771 180.839 853.47 180.839C835.427 180.839 806.894 185.455 806.894 203.077C806.894 219.441 825.776 224.895 842.141 226.993L889.975 232.028C965.922 238.742 999.491 268.532 999.491 326.854C999.491 393.986 932.774 419.161 866.896 419.161C801.019 419.161 732.624 389.79 719.616 330.21L718.357 323.916H803.117C814.866 357.483 868.994 354.965 866.896 354.965C898.786 354.965 918.508 345.315 918.508 329.79C918.508 319.72 915.15 307.972 882.422 303.776L832.489 298.741C766.612 294.126 726.75 259.3 726.75 207.272C726.75 143.496 787.173 116.644 853.47 116.644ZM251.761 0C331.485 0 378.9 47.4126 378.9 110.35C378.9 173.287 331.485 220.7 251.761 220.7H211.479L411.629 411.609H270.224L117.908 266.854C106.999 256.784 101.963 245.036 101.963 234.966C101.963 220.7 112.034 208.113 131.335 202.658L209.801 181.679C239.592 173.707 260.153 150.63 260.153 120.42C260.153 83.4968 229.941 62.0977 192.597 62.0977H0V0H251.761ZM1520.63 118.74C1591.97 118.74 1641.9 170.768 1641.9 245.034V411.608H1560.5V258.88C1560.5 216.922 1538.26 191.747 1498.81 191.747C1459.37 191.747 1433.78 217.761 1433.78 258.88V411.608H1354.05V124.614H1435.04L1431.26 170.349C1443.01 147.272 1479.51 118.74 1520.63 118.74ZM1826.52 191.749C1777.43 191.749 1751.42 228.252 1751.42 269.791C1751.42 314.266 1780.79 348.252 1826.52 348.252C1870.58 348.252 1899.11 313.847 1899.11 269.791C1899.11 225.735 1871.42 191.749 1826.52 191.749ZM543.803 183.355C504.138 183.355 481.475 207.041 474.442 238.74H611.728C610.893 235.09 609.818 231.188 608.421 226.992C598.35 198.461 574.853 183.356 543.803 183.355ZM1173.62 183.355C1134.3 183.356 1111.7 207.041 1104.68 238.74H1241.97C1241.13 235.09 1240.06 231.188 1238.66 226.992C1228.59 198.461 1205.09 183.355 1173.62 183.355Z" fill="#FDFDFD"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="1800" height="1800" viewBox="0 0 1800 1800" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M1000.46 450C1174.77 450 1278.43 553.669 1278.43 691.282C1278.43 828.896 1174.77 932.563 1000.46 932.563H912.382L1350 1350H1040.82L707.794 1033.48C683.944 1011.47 672.936 985.781 672.935 963.765C672.935 932.572 694.959 905.049 737.161 893.122L908.712 847.244C973.85 829.812 1018.81 779.353 1018.81 713.298C1018.8 632.567 952.745 585.78 871.095 585.78H450V450H1000.46Z" fill="black"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Resend Tool browser app: hash router, chrome and world watching. Every screen reads and writes through the Tool's
|
|
2
|
+
// own operations via /_firedrill/client.js; nothing here is authoritative and records are re-read after each write.
|
|
3
|
+
import { hydrateIcons } from "./icons.js";
|
|
4
|
+
import { $, call, clock, el, notSimulated, watchWorld } from "./ui.js";
|
|
5
|
+
import { renderEmails, renderEmailDetail } from "./view-emails.js";
|
|
6
|
+
import { renderDomains, renderDomainDetail } from "./view-domains.js";
|
|
7
|
+
import { renderApiKeys } from "./view-keys.js";
|
|
8
|
+
import { renderContacts, renderSegments } from "./view-audience.js";
|
|
9
|
+
|
|
10
|
+
export const workspace = { value: null, error: null };
|
|
11
|
+
|
|
12
|
+
async function loadWorkspace() {
|
|
13
|
+
try {
|
|
14
|
+
const value = await call("workspace.context");
|
|
15
|
+
workspace.value = value;
|
|
16
|
+
workspace.error = null;
|
|
17
|
+
clock.set(value.now);
|
|
18
|
+
$("#team-name").textContent = value.team.name;
|
|
19
|
+
$("#team-avatar").textContent = value.team.name.charAt(0).toUpperCase();
|
|
20
|
+
$("#user-name").textContent = value.apiKey.name;
|
|
21
|
+
$("#user-avatar").textContent = value.apiKey.name.charAt(0).toUpperCase();
|
|
22
|
+
const usage = $("#usage");
|
|
23
|
+
usage.replaceChildren(
|
|
24
|
+
el("div", { class: "usage-plan", text: `${value.team.plan.charAt(0).toUpperCase() + value.team.plan.slice(1)} plan` }),
|
|
25
|
+
el("div", {}, ["Daily emails ", el("strong", { text: `${value.team.sentToday} / ${value.team.dailyQuota}` })]),
|
|
26
|
+
el("div", { class: "meter", attrs: { role: "presentation" } }, [el("span")]),
|
|
27
|
+
);
|
|
28
|
+
usage.querySelector(".meter span").style.width = `${Math.min(100, (100 * value.team.sentToday) / value.team.dailyQuota)}%`;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
workspace.error = error;
|
|
31
|
+
$("#team-name").textContent = "Resend";
|
|
32
|
+
$("#team-avatar").textContent = "?";
|
|
33
|
+
$("#user-name").textContent = "No access";
|
|
34
|
+
$("#user-avatar").textContent = "?";
|
|
35
|
+
$("#usage").replaceChildren();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const ROUTES = [
|
|
40
|
+
[/^#\/emails\/([^/]+)$/, "emails", (m, v, o) => renderEmailDetail(decodeURIComponent(m[1]), v, o)],
|
|
41
|
+
[/^#\/emails$/, "emails", (m, v, o) => renderEmails(m, v, o)],
|
|
42
|
+
[/^#\/domains\/([^/]+)$/, "domains", (m, v, o) => renderDomainDetail(decodeURIComponent(m[1]), v, o)],
|
|
43
|
+
[/^#\/domains$/, "domains", (m, v, o) => renderDomains(m, v, o)],
|
|
44
|
+
[/^#\/api-keys$/, "api-keys", (m, v, o) => renderApiKeys(m, v, o)],
|
|
45
|
+
[/^#\/audience\/segments$/, "segments", (m, v, o) => renderSegments(m, v, o)],
|
|
46
|
+
[/^#\/audience(\/contacts)?$/, "contacts", (m, v, o) => renderContacts(m, v, o)],
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
let current = null;
|
|
50
|
+
async function route() {
|
|
51
|
+
const hash = location.hash || "#/emails";
|
|
52
|
+
const match = ROUTES.map(([re, name, render]) => ({ m: re.exec(hash), name, render })).find((r) => r.m);
|
|
53
|
+
if (!match) {
|
|
54
|
+
location.replace("#/emails");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
for (const link of document.querySelectorAll("#nav a[data-route]")) {
|
|
58
|
+
const on = link.dataset.route === match.name || (link.dataset.route === "audience" && (match.name === "contacts" || match.name === "segments"));
|
|
59
|
+
link.classList.toggle("active", on && !(link.dataset.route === "audience"));
|
|
60
|
+
if (on) link.setAttribute("aria-current", "page"); else link.removeAttribute("aria-current");
|
|
61
|
+
}
|
|
62
|
+
document.body.classList.remove("nav-open");
|
|
63
|
+
current = match;
|
|
64
|
+
const view = $("#view");
|
|
65
|
+
try {
|
|
66
|
+
await match.render(match.m, view);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
view.replaceChildren(el("p", { class: "muted", text: error?.message ?? "Failed to render." }));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Views call this to re-render the current route in place (after a write or a world revision change). */
|
|
73
|
+
export async function refresh() {
|
|
74
|
+
await loadWorkspace();
|
|
75
|
+
if (current) await current.render(current.m, $("#view"), { refresh: true });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
document.addEventListener("click", (event) => {
|
|
79
|
+
const soon = event.target.closest("[data-soon]");
|
|
80
|
+
if (soon) {
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
void notSimulated(soon.dataset.soon);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
$("#mobile-menu").addEventListener("click", () => document.body.classList.toggle("nav-open"));
|
|
86
|
+
hydrateIcons();
|
|
87
|
+
window.addEventListener("hashchange", () => void route());
|
|
88
|
+
await loadWorkspace();
|
|
89
|
+
await route();
|
|
90
|
+
watchWorld(refresh);
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="1800" height="1800" viewBox="0 0 1800 1800" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M1000.46 450C1174.77 450 1278.43 553.669 1278.43 691.282C1278.43 828.896 1174.77 932.563 1000.46 932.563H912.382L1350 1350H1040.82L707.794 1033.48C683.944 1011.47 672.936 985.781 672.935 963.765C672.935 932.572 694.959 905.049 737.161 893.122L908.712 847.244C973.85 829.812 1018.81 779.353 1018.81 713.298C1018.8 632.567 952.745 585.78 871.095 585.78H450V450H1000.46Z" fill="#FDFDFD"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="1978" height="420" viewBox="0 0 1978 420" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M543.803 118.321C633.178 118.321 698.215 180.419 693.6 293.286H475.948C484.076 324.654 505.791 351.608 549.257 351.608C573.594 351.608 595.833 344.895 612.617 323.916H687.726L686.467 330.629C673.459 388.112 608.001 420 549.257 420C456.106 420 393.585 357.062 393.585 269.37C393.585 181.678 456.106 118.321 543.803 118.321ZM1173.62 118.321C1263.42 118.321 1328.04 180.419 1323.84 293.286H1106.13C1114.16 324.654 1135.72 351.608 1179.5 351.608C1203.84 351.608 1226.07 344.895 1242.44 323.916H1317.97L1316.71 330.629C1303.7 388.111 1238.24 420 1179.5 420C1086.35 420 1023.83 357.063 1023.83 269.37C1023.83 181.678 1086.35 118.321 1173.62 118.321ZM1978 411.609H1896.6L1900.79 370.91C1889.46 395.246 1852.96 419.162 1807.22 419.162C1731.69 419.162 1670.43 361.26 1670.43 269.791C1670.43 178.322 1730.85 120.42 1807.22 120.42C1848.76 120.42 1874.36 133.847 1897.02 156.924V0H1978V411.609ZM853.47 116.644C919.767 116.644 978.091 140.559 994.875 199.301L996.554 205.595H913.892C896.268 183.777 872.771 180.839 853.47 180.839C835.427 180.839 806.894 185.455 806.894 203.077C806.894 219.441 825.776 224.895 842.141 226.993L889.975 232.028C965.922 238.742 999.491 268.532 999.491 326.854C999.491 393.986 932.774 419.161 866.896 419.161C801.019 419.161 732.624 389.79 719.616 330.21L718.357 323.916H803.117C814.866 357.483 868.994 354.965 866.896 354.965C898.786 354.965 918.508 345.315 918.508 329.79C918.508 319.72 915.15 307.972 882.422 303.776L832.489 298.741C766.612 294.126 726.75 259.3 726.75 207.272C726.75 143.496 787.173 116.644 853.47 116.644ZM251.761 0C331.485 0 378.9 47.4126 378.9 110.35C378.9 173.287 331.485 220.7 251.761 220.7H211.479L411.629 411.609H270.224L117.908 266.854C106.999 256.784 101.963 245.036 101.963 234.966C101.963 220.7 112.034 208.113 131.335 202.658L209.801 181.679C239.592 173.707 260.153 150.63 260.153 120.42C260.153 83.4968 229.941 62.0977 192.597 62.0977H0V0H251.761ZM1520.63 118.74C1591.97 118.74 1641.9 170.768 1641.9 245.034V411.608H1560.5V258.88C1560.5 216.922 1538.26 191.747 1498.81 191.747C1459.37 191.747 1433.78 217.761 1433.78 258.88V411.608H1354.05V124.614H1435.04L1431.26 170.349C1443.01 147.272 1479.51 118.74 1520.63 118.74ZM1826.52 191.749C1777.43 191.749 1751.42 228.252 1751.42 269.791C1751.42 314.266 1780.79 348.252 1826.52 348.252C1870.58 348.252 1899.11 313.847 1899.11 269.791C1899.11 225.735 1871.42 191.749 1826.52 191.749ZM543.803 183.355C504.138 183.355 481.475 207.041 474.442 238.74H611.728C610.893 235.09 609.818 231.188 608.421 226.992C598.35 198.461 574.853 183.356 543.803 183.355ZM1173.62 183.355C1134.3 183.356 1111.7 207.041 1104.68 238.74H1241.97C1241.13 235.09 1240.06 231.188 1238.66 226.992C1228.59 198.461 1205.09 183.355 1173.62 183.355Z" fill="#FDFDFD"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="1800" height="1800" viewBox="0 0 1800 1800" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M1000.46 450C1174.77 450 1278.43 553.669 1278.43 691.282C1278.43 828.896 1174.77 932.563 1000.46 932.563H912.382L1350 1350H1040.82L707.794 1033.48C683.944 1011.47 672.936 985.781 672.935 963.765C672.935 932.572 694.959 905.049 737.161 893.122L908.712 847.244C973.85 829.812 1018.81 779.353 1018.81 713.298C1018.8 632.567 952.745 585.78 871.095 585.78H450V450H1000.46Z" fill="black"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Contact side panel: edit names and subscription, manage segment membership, delete. Resolves true when anything changed.
|
|
2
|
+
import { allSegments } from "./view-audience.js";
|
|
3
|
+
import { icon } from "./icons.js";
|
|
4
|
+
import { badge, button, call, confirmDialog, describe, el, errorState, field, formatDate, guard, modal, mutate, toast } from "./ui.js";
|
|
5
|
+
|
|
6
|
+
async function contactSegments(id) {
|
|
7
|
+
const out = [];
|
|
8
|
+
let after;
|
|
9
|
+
for (let page = 0; page < 50; page += 1) {
|
|
10
|
+
const list = await call("contacts.list_segments", { contactId: id, limit: 100, ...(after ? { after } : {}) });
|
|
11
|
+
out.push(...list.data);
|
|
12
|
+
if (!list.has_more || list.data.length === 0) break;
|
|
13
|
+
after = list.data[list.data.length - 1].id;
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function openContact(id) {
|
|
19
|
+
let changed = false;
|
|
20
|
+
let contact;
|
|
21
|
+
let member = [];
|
|
22
|
+
let segs = [];
|
|
23
|
+
let loadError = null;
|
|
24
|
+
try {
|
|
25
|
+
[contact, member, segs] = await Promise.all([call("contacts.get", { id }), contactSegments(id), allSegments()]);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
loadError = error;
|
|
28
|
+
}
|
|
29
|
+
await modal(loadError ? "Contact" : contact.email, (close) => {
|
|
30
|
+
if (loadError) return [errorState(loadError), el("div", { class: "dialog-actions" }, [button("Close", { onClick: () => close() })])];
|
|
31
|
+
const first = el("input", { class: "input", attrs: { id: "edit-first", autocomplete: "off" } });
|
|
32
|
+
const last = el("input", { class: "input", attrs: { id: "edit-last", autocomplete: "off" } });
|
|
33
|
+
first.value = contact.first_name ?? "";
|
|
34
|
+
last.value = contact.last_name ?? "";
|
|
35
|
+
const subscribed = el("input", { attrs: { id: "edit-subscribed", type: "checkbox" } });
|
|
36
|
+
subscribed.checked = !contact.unsubscribed;
|
|
37
|
+
const error = el("p", { class: "form-error", attrs: { role: "alert" } });
|
|
38
|
+
const chips = el("div", { class: "chips" });
|
|
39
|
+
const adder = el("select", { class: "select", attrs: { "aria-label": "Add to segment" } });
|
|
40
|
+
const drawChips = () => {
|
|
41
|
+
chips.replaceChildren(...(member.length ? member.map((s) => el("span", { class: "chip" }, [el("span", { text: s.name }), el("button", { class: "icon-btn copy", attrs: { type: "button", "aria-label": `Remove from ${s.name}`, title: `Remove from ${s.name}` }, on: { click: () => void membership("contacts.remove_segment", s) } }, [icon("x", 12)])])) : [el("span", { class: "muted", text: "Not in any segment" })]));
|
|
42
|
+
const available = segs.filter((s) => !member.some((m) => m.id === s.id));
|
|
43
|
+
adder.replaceChildren(el("option", { text: available.length ? "Add to segment…" : "No other segments", attrs: { value: "" } }), ...available.map((s) => el("option", { text: s.name, attrs: { value: s.id } })));
|
|
44
|
+
};
|
|
45
|
+
const membership = async (op, segment) => {
|
|
46
|
+
error.textContent = "";
|
|
47
|
+
try {
|
|
48
|
+
await guard(() => mutate(op, { contactId: id, segmentId: segment.id }));
|
|
49
|
+
changed = true;
|
|
50
|
+
member = await contactSegments(id);
|
|
51
|
+
drawChips();
|
|
52
|
+
} catch (e) { error.textContent = describe(e); }
|
|
53
|
+
};
|
|
54
|
+
adder.addEventListener("change", () => { const s = segs.find((x) => x.id === adder.value); if (s) void membership("contacts.add_segment", s); });
|
|
55
|
+
drawChips();
|
|
56
|
+
const save = button("Save", { variant: "primary" });
|
|
57
|
+
save.type = "submit";
|
|
58
|
+
const del = button("Delete", { variant: "ghost", iconName: "trash", onClick: async () => {
|
|
59
|
+
if (!(await confirmDialog("Delete contact", `Delete ${contact.email}? This removes the contact from every segment.`, "Delete contact"))) return;
|
|
60
|
+
try { await guard(() => mutate("contacts.remove", { id })); changed = true; toast("Contact deleted"); close(); } catch (e) { error.textContent = describe(e); }
|
|
61
|
+
} });
|
|
62
|
+
const props = Object.entries(contact.properties ?? {});
|
|
63
|
+
const form = el("form", {}, [
|
|
64
|
+
el("div", { class: "panel-meta" }, [badge(contact.unsubscribed ? "unsubscribed" : "subscribed"), el("span", { class: "muted", text: `Added ${formatDate(contact.created_at)}` })]),
|
|
65
|
+
el("div", { class: "two" }, [field("First name", first), field("Last name", last)]),
|
|
66
|
+
el("label", { class: "check toggle", attrs: { for: "edit-subscribed" } }, [subscribed, el("span", { text: "Subscribed" })]),
|
|
67
|
+
el("div", { class: "field" }, [el("span", { class: "label", text: "Segments" }), chips, adder]),
|
|
68
|
+
props.length ? el("div", { class: "field" }, [el("span", { class: "label", text: "Properties" }), el("dl", { class: "props" }, props.flatMap(([k, v]) => [el("dt", { class: "mono", text: k }), el("dd", { text: v === null ? "—" : String(v) })]))]) : null,
|
|
69
|
+
error,
|
|
70
|
+
el("div", { class: "dialog-actions split" }, [del, el("span", { class: "spacer" }), button("Close", { onClick: () => close() }), save]),
|
|
71
|
+
]);
|
|
72
|
+
form.addEventListener("submit", async (event) => {
|
|
73
|
+
event.preventDefault();
|
|
74
|
+
save.disabled = true;
|
|
75
|
+
error.textContent = "";
|
|
76
|
+
try {
|
|
77
|
+
await guard(() => mutate("contacts.update", { id, firstName: first.value.trim() || null, lastName: last.value.trim() || null, unsubscribed: !subscribed.checked }));
|
|
78
|
+
changed = true;
|
|
79
|
+
toast("Contact updated");
|
|
80
|
+
close();
|
|
81
|
+
} catch (e) { error.textContent = describe(e); save.disabled = false; }
|
|
82
|
+
});
|
|
83
|
+
return form;
|
|
84
|
+
}, { wide: true });
|
|
85
|
+
return changed;
|
|
86
|
+
}
|