@asd20/ui-next 2.5.0 → 2.6.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,12 @@
1
1
  # Changelog
2
2
 
3
+ # [2.6.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.5.0...ui-next-v2.6.0) (2026-05-04)
4
+
5
+
6
+ ### Features
7
+
8
+ * add rate limit to emails and associated messaging ([66e4099](https://github.com/academydistrict20/asd20-ui-next/commit/66e40994ac924a9dad184227beae4dded0dbf2d9))
9
+
3
10
  # [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
11
 
5
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asd20/ui-next",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "private": false,
5
5
  "description": "ASD20 UI component library for Vue 3.",
6
6
  "license": "MIT",
@@ -32,24 +32,37 @@
32
32
  required
33
33
  @validated="validationErrors.messageBody = $event"
34
34
  />
35
- <Recaptcha
36
- sitekey="6LfidKoUAAAAAFqr3QEbia3jIkecsZyxBYlMvWrX"
37
- :load-recaptcha-script="true"
38
- size="invisible"
39
- @verify="captchaVerified"
35
+ <div
36
+ class="asd20-compose-email-modal__submit"
37
+ aria-live="polite"
40
38
  >
41
- <asd20-button
42
- :disabled="!isValid || sending"
43
- label="Send"
44
- horizontal
45
- centered
46
- bordered
39
+ <Recaptcha
40
+ v-if="!sending && !sendSucceeded"
41
+ sitekey="6LfidKoUAAAAAFqr3QEbia3jIkecsZyxBYlMvWrX"
42
+ :load-recaptcha-script="true"
43
+ size="invisible"
44
+ @verify="captchaVerified"
45
+ >
46
+ <asd20-button
47
+ :disabled="!isValid"
48
+ label="Send"
49
+ horizontal
50
+ centered
51
+ bordered
52
+ />
53
+ </Recaptcha>
54
+ <asd20-spinner
55
+ v-else-if="sending"
56
+ size="sm"
47
57
  />
48
- </Recaptcha>
49
- <asd20-spinner
50
- v-if="sending"
51
- size="sm"
52
- />
58
+ <p
59
+ v-else
60
+ class="asd20-compose-email-modal__success"
61
+ role="status"
62
+ >
63
+ Your message was sent.
64
+ </p>
65
+ </div>
53
66
  </Asd20Viewport>
54
67
  </asd20-modal>
55
68
  </template>
@@ -87,6 +100,7 @@ export default {
87
100
  },
88
101
  composing: false,
89
102
  sending: false,
103
+ sendSucceeded: false,
90
104
  emailMessage: {
91
105
  senderName: '',
92
106
  senderEmail: '',
@@ -102,6 +116,11 @@ export default {
102
116
  )
103
117
  },
104
118
  },
119
+ watch: {
120
+ open(value) {
121
+ if (value) this.resetSendState()
122
+ },
123
+ },
105
124
  methods: {
106
125
  resolveRuntimeConfig() {
107
126
  const runtimeConfig =
@@ -129,7 +148,8 @@ export default {
129
148
  )
130
149
  },
131
150
  validateEmailAddress({ value, validationErrors }) {
132
- const pattern = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
151
+ const pattern =
152
+ /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
133
153
  const regex = RegExp(pattern)
134
154
  if (!regex.test(value))
135
155
  validationErrors.push('A valid email address is required')
@@ -149,15 +169,36 @@ export default {
149
169
  captchaVerified(response) {
150
170
  if (response) this.sendEmail(response)
151
171
  },
172
+ resetSendState() {
173
+ this.sending = false
174
+ this.sendSucceeded = false
175
+ },
176
+ waitForSuccessMessage() {
177
+ return new Promise(resolve => {
178
+ setTimeout(resolve, 1200)
179
+ })
180
+ },
181
+ getSendEmailErrorStatus(error) {
182
+ if (error?.response?.status) return Number(error.response.status)
183
+ if (error?.status) return Number(error.status)
184
+
185
+ const statusMatch = String(error?.message || '').match(/\b(\d{3})\b/)
186
+ return statusMatch ? Number(statusMatch[1]) : null
187
+ },
188
+ getSendEmailFailureMessage(error) {
189
+ if (this.getSendEmailErrorStatus(error) === 429) {
190
+ return 'We could not send your message right now. Please wait and try again later.'
191
+ }
192
+
193
+ return 'Something went wrong while sending your email. Please try again later.'
194
+ },
152
195
  async sendEmail(captchaToken = '') {
153
196
  if (!this.isValid) return
154
197
 
155
198
  const endpoint = this.resolveSendEmailEndpoint()
156
199
  const sendEmailClient =
157
200
  this.$sendEmail ||
158
- (endpoint
159
- ? async (message) => await sendEmail(message, endpoint)
160
- : null)
201
+ (endpoint ? async message => await sendEmail(message, endpoint) : null)
161
202
 
162
203
  if (!sendEmailClient) {
163
204
  console.error('Send email endpoint is not configured.')
@@ -168,6 +209,7 @@ export default {
168
209
  }
169
210
 
170
211
  this.sending = true
212
+ this.sendSucceeded = false
171
213
  try {
172
214
  await sendEmailClient(
173
215
  Object.assign({}, this.emailMessage, {
@@ -175,12 +217,14 @@ export default {
175
217
  captchaToken,
176
218
  })
177
219
  )
220
+ this.sending = false
221
+ this.sendSucceeded = true
222
+ await this.waitForSuccessMessage()
178
223
  this.$emit('dismiss')
179
224
  } catch (error) {
225
+ this.sendSucceeded = false
180
226
  console.error('Email send failed:', error?.message || error)
181
- alert(
182
- 'Something went wrong while sending your email. Please try again later.'
183
- )
227
+ alert(this.getSendEmailFailureMessage(error))
184
228
  } finally {
185
229
  this.sending = false
186
230
  }
@@ -196,6 +240,19 @@ export default {
196
240
  & :deep(.asd20-modal__content .asd20-viewport) {
197
241
  padding: space(1);
198
242
  }
243
+
244
+ &__submit {
245
+ align-items: center;
246
+ display: flex;
247
+ justify-content: center;
248
+ min-height: 3rem;
249
+ }
250
+
251
+ &__success {
252
+ font-weight: 600;
253
+ margin: 0;
254
+ text-align: center;
255
+ }
199
256
  }
200
257
 
201
258
  @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(