@asd20/ui-next 2.5.0 → 2.7.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ # [2.7.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.6.0...ui-next-v2.7.0) (2026-05-05)
4
+
5
+
6
+ ### Features
7
+
8
+ * implement AI evaluation of email messages ([4ed90b4](https://github.com/academydistrict20/asd20-ui-next/commit/4ed90b413da2b2690b3655c1c701003cbdbb7bd6))
9
+
10
+ # [2.6.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.5.0...ui-next-v2.6.0) (2026-05-04)
11
+
12
+
13
+ ### Features
14
+
15
+ * add rate limit to emails and associated messaging ([66e4099](https://github.com/academydistrict20/asd20-ui-next/commit/66e40994ac924a9dad184227beae4dded0dbf2d9))
16
+
3
17
  # [2.5.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.4.3...ui-next-v2.5.0) (2026-05-04)
4
18
 
5
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asd20/ui-next",
3
- "version": "2.5.0",
3
+ "version": "2.7.0",
4
4
  "private": false,
5
5
  "description": "ASD20 UI component library for Vue 3.",
6
6
  "license": "MIT",
@@ -9,47 +9,91 @@
9
9
  @dismiss="$emit('dismiss')"
10
10
  >
11
11
  <Asd20Viewport scrollable>
12
- <asd20-text-input
13
- id="senderName"
14
- v-model="emailMessage.senderName"
15
- label="Your Full Name"
16
- required
17
- @validated="validationErrors.senderName = $event"
18
- />
19
- <asd20-text-input
20
- id="senderEmail"
21
- v-model="emailMessage.senderEmail"
22
- type="email"
23
- :validator="validateEmailAddress"
24
- label="Your Email Address"
25
- required
26
- @validated="validationErrors.senderEmail = $event"
27
- />
28
- <asd20-text-area-input
29
- id="messageBody"
30
- v-model="emailMessage.messageBody"
31
- label="Message"
32
- required
33
- @validated="validationErrors.messageBody = $event"
34
- />
35
- <Recaptcha
36
- sitekey="6LfidKoUAAAAAFqr3QEbia3jIkecsZyxBYlMvWrX"
37
- :load-recaptcha-script="true"
38
- size="invisible"
39
- @verify="captchaVerified"
40
- >
41
- <asd20-button
42
- :disabled="!isValid || sending"
43
- label="Send"
44
- horizontal
45
- centered
46
- bordered
12
+ <template v-if="!sendRejected">
13
+ <asd20-text-input
14
+ id="senderName"
15
+ v-model="emailMessage.senderName"
16
+ label="Your Full Name"
17
+ required
18
+ @validated="validationErrors.senderName = $event"
19
+ />
20
+ <asd20-text-input
21
+ id="senderEmail"
22
+ v-model="emailMessage.senderEmail"
23
+ type="email"
24
+ :validator="validateEmailAddress"
25
+ label="Your Email Address"
26
+ required
27
+ @validated="validationErrors.senderEmail = $event"
47
28
  />
48
- </Recaptcha>
49
- <asd20-spinner
50
- v-if="sending"
51
- size="sm"
52
- />
29
+ <asd20-text-area-input
30
+ id="messageBody"
31
+ v-model="emailMessage.messageBody"
32
+ label="Message"
33
+ required
34
+ @validated="validationErrors.messageBody = $event"
35
+ />
36
+ <div
37
+ class="asd20-compose-email-modal__submit"
38
+ aria-live="polite"
39
+ >
40
+ <Recaptcha
41
+ v-if="!sending && !sendSucceeded"
42
+ sitekey="6LfidKoUAAAAAFqr3QEbia3jIkecsZyxBYlMvWrX"
43
+ :load-recaptcha-script="true"
44
+ size="invisible"
45
+ @verify="captchaVerified"
46
+ >
47
+ <asd20-button
48
+ :disabled="!isValid"
49
+ label="Send"
50
+ horizontal
51
+ centered
52
+ bordered
53
+ />
54
+ </Recaptcha>
55
+ <asd20-spinner
56
+ v-else-if="sending"
57
+ size="sm"
58
+ />
59
+ <p
60
+ v-else
61
+ class="asd20-compose-email-modal__success"
62
+ role="status"
63
+ >
64
+ Your message was sent.
65
+ </p>
66
+ </div>
67
+ </template>
68
+ <div
69
+ v-else
70
+ class="asd20-compose-email-modal__rejection"
71
+ role="status"
72
+ >
73
+ <p
74
+ class="asd20-compose-email-modal__rejection-message"
75
+ >
76
+ Please contact the Academy District 20 Help Desk who can evaluate your
77
+ request and route it appropriately.
78
+ </p>
79
+ <div class="asd20-compose-email-modal__rejection-actions">
80
+ <asd20-button
81
+ label="Contact Our Help Desk"
82
+ :link="helpDeskUrl"
83
+ horizontal
84
+ centered
85
+ bordered
86
+ />
87
+ <asd20-button
88
+ label="Cancel"
89
+ horizontal
90
+ centered
91
+ bordered
92
+ transparent
93
+ @click="$emit('dismiss')"
94
+ />
95
+ </div>
96
+ </div>
53
97
  </Asd20Viewport>
54
98
  </asd20-modal>
55
99
  </template>
@@ -87,6 +131,9 @@ export default {
87
131
  },
88
132
  composing: false,
89
133
  sending: false,
134
+ sendSucceeded: false,
135
+ sendRejected: false,
136
+ helpDeskUrl: 'https://asd20.org/help-desk',
90
137
  emailMessage: {
91
138
  senderName: '',
92
139
  senderEmail: '',
@@ -102,6 +149,11 @@ export default {
102
149
  )
103
150
  },
104
151
  },
152
+ watch: {
153
+ open(value) {
154
+ if (value) this.resetSendState()
155
+ },
156
+ },
105
157
  methods: {
106
158
  resolveRuntimeConfig() {
107
159
  const runtimeConfig =
@@ -129,7 +181,8 @@ export default {
129
181
  )
130
182
  },
131
183
  validateEmailAddress({ value, validationErrors }) {
132
- const pattern = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
184
+ const pattern =
185
+ /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
133
186
  const regex = RegExp(pattern)
134
187
  if (!regex.test(value))
135
188
  validationErrors.push('A valid email address is required')
@@ -149,15 +202,40 @@ export default {
149
202
  captchaVerified(response) {
150
203
  if (response) this.sendEmail(response)
151
204
  },
205
+ resetSendState() {
206
+ this.sending = false
207
+ this.sendSucceeded = false
208
+ this.sendRejected = false
209
+ },
210
+ waitForSuccessMessage() {
211
+ return new Promise(resolve => {
212
+ setTimeout(resolve, 1200)
213
+ })
214
+ },
215
+ getSendEmailErrorStatus(error) {
216
+ if (error?.response?.status) return Number(error.response.status)
217
+ if (error?.status) return Number(error.status)
218
+
219
+ const statusMatch = String(error?.message || '').match(/\b(\d{3})\b/)
220
+ return statusMatch ? Number(statusMatch[1]) : null
221
+ },
222
+ getSendEmailFailureMessage(error) {
223
+ if (this.getSendEmailErrorStatus(error) === 429) {
224
+ return 'We could not send your message right now. Please wait and try again later.'
225
+ }
226
+
227
+ return 'Something went wrong while sending your email. Please try again later.'
228
+ },
229
+ isHelpDeskRejection(error) {
230
+ return error?.response?.data?.code === 'moderation_rejected'
231
+ },
152
232
  async sendEmail(captchaToken = '') {
153
233
  if (!this.isValid) return
154
234
 
155
235
  const endpoint = this.resolveSendEmailEndpoint()
156
236
  const sendEmailClient =
157
237
  this.$sendEmail ||
158
- (endpoint
159
- ? async (message) => await sendEmail(message, endpoint)
160
- : null)
238
+ (endpoint ? async message => await sendEmail(message, endpoint) : null)
161
239
 
162
240
  if (!sendEmailClient) {
163
241
  console.error('Send email endpoint is not configured.')
@@ -168,6 +246,7 @@ export default {
168
246
  }
169
247
 
170
248
  this.sending = true
249
+ this.sendSucceeded = false
171
250
  try {
172
251
  await sendEmailClient(
173
252
  Object.assign({}, this.emailMessage, {
@@ -175,12 +254,20 @@ export default {
175
254
  captchaToken,
176
255
  })
177
256
  )
257
+ this.sending = false
258
+ this.sendSucceeded = true
259
+ await this.waitForSuccessMessage()
178
260
  this.$emit('dismiss')
179
261
  } catch (error) {
262
+ this.sendSucceeded = false
180
263
  console.error('Email send failed:', error?.message || error)
181
- alert(
182
- 'Something went wrong while sending your email. Please try again later.'
183
- )
264
+ if (this.isHelpDeskRejection(error)) {
265
+ this.helpDeskUrl =
266
+ error?.response?.data?.helpDeskUrl || 'https://asd20.org/help-desk'
267
+ this.sendRejected = true
268
+ return
269
+ }
270
+ alert(this.getSendEmailFailureMessage(error))
184
271
  } finally {
185
272
  this.sending = false
186
273
  }
@@ -196,6 +283,35 @@ export default {
196
283
  & :deep(.asd20-modal__content .asd20-viewport) {
197
284
  padding: space(1);
198
285
  }
286
+
287
+ &__submit {
288
+ align-items: center;
289
+ display: flex;
290
+ justify-content: center;
291
+ min-height: 3rem;
292
+ }
293
+
294
+ &__success {
295
+ font-weight: 600;
296
+ margin: 0;
297
+ text-align: center;
298
+ }
299
+
300
+ &__rejection {
301
+ display: flex;
302
+ flex-direction: column;
303
+ gap: space(1);
304
+ }
305
+
306
+ &__rejection-message {
307
+ margin: 0;
308
+ }
309
+
310
+ &__rejection-actions {
311
+ display: flex;
312
+ flex-wrap: wrap;
313
+ gap: space(0.75);
314
+ }
199
315
  }
200
316
 
201
317
  @media (min-width: 1024px) {
@@ -9,7 +9,7 @@ function emailMessageIsValid(emailMessage) {
9
9
  )
10
10
  }
11
11
 
12
- export default async function(emailMessage, endpoint) {
12
+ export default async function (emailMessage, endpoint) {
13
13
  // Check to see if the parameters is valid
14
14
  if (!emailMessageIsValid(emailMessage))
15
15
  throw new Error(
@@ -20,13 +20,15 @@ export default async function(emailMessage, endpoint) {
20
20
  await axios.post(endpoint, emailMessage)
21
21
  } catch (error) {
22
22
  if (error.response) {
23
- throw new Error(
23
+ const sendError = new Error(
24
24
  `A ${
25
25
  error.response.status
26
26
  } error occurred while attempting to send email: ${JSON.stringify(
27
27
  error.response.data
28
28
  )}`
29
29
  )
30
+ sendError.response = error.response
31
+ throw sendError
30
32
  } else {
31
33
  // Something happened in setting up the request and triggered an Error
32
34
  throw new Error(