Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion @types/mjml-template.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
declare module '*.mjml?raw' {
const content: string
export default content
}
}
2 changes: 1 addition & 1 deletion @types/pagedjs.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ declare module 'pagedjs' {
}

export const registeredHandlers: PagedJsHandlerConstructor[]
}
}
2 changes: 1 addition & 1 deletion _TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ https://aws.plainenglish.io/how-to-build-a-chatbot-using-aws-lex-and-lambda-in-2
## Contact Form

- `0/2000` characters should show number of characters left instead
- Workflow right now puts the "Success" toast under the submit button when the submit button returns to normal after a submission. It seems like the button should have some time out after a successful submission to make sure it's not hammered, like five seconds. And it just looks visually odd - maybe the button should be part of the layout of the success toast, or moved down under it.

## Newsletter / MJML Templates

- We need to make sure the images point to the full production URL, not a relative import
- Need to move the unsubscribe link into an Action and handle it entirely within our website instead of on Hubspot
- Need to add a newsletter publishing workflow as an action, using the newsletter static segment imported from Hubspot
Binary file removed cover.jpg
Binary file not shown.
Binary file added public/pdf/resume.pdf
Binary file not shown.
8 changes: 5 additions & 3 deletions src/actions/contact/__tests__/action.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ type ContactSubmitOutput = {
message: string
}

const getMockedHandler = <Input, Output>(action: unknown): ActionConfig<Input, Output>['handler'] => {
const getMockedHandler = <Input, Output>(
action: unknown
): ActionConfig<Input, Output>['handler'] => {
return (action as ActionConfig<Input, Output>).handler
}

Expand Down Expand Up @@ -106,7 +108,7 @@ vi.mock('@actions/utils/errors', async () => {
? messageOrError
: messageOrError instanceof Error
? messageOrError.message
: options?.message ?? 'Internal server error'
: (options?.message ?? 'Internal server error')
super(message)
this.name = 'ActionsFunctionError'
this.status = options?.status ?? 500
Expand Down Expand Up @@ -239,4 +241,4 @@ describe('contact.submit.handler', () => {
})
)
})
})
})
8 changes: 6 additions & 2 deletions src/actions/contact/__tests__/domain.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ describe('contact domain validation', () => {
if (result.success) {
throw new Error('Expected schema validation to fail')
}
expect(z.flattenError(result.error).fieldErrors['timeline']).toContain('Invalid project timeline')
expect(z.flattenError(result.error).fieldErrors['timeline']).toContain(
'Invalid project timeline'
)
})

it('rejects messages that appear to contain spam', () => {
Expand All @@ -65,6 +67,8 @@ describe('contact domain validation', () => {
if (result.success) {
throw new Error('Expected schema validation to fail')
}
expect(z.flattenError(result.error).fieldErrors['message']).toContain('Message appears to contain spam')
expect(z.flattenError(result.error).fieldErrors['message']).toContain(
'Message appears to contain spam'
)
})
})
4 changes: 1 addition & 3 deletions src/actions/contact/__tests__/responder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,7 @@ describe('contact responder', () => {
{ label: 'Budget', value: '$5k-$10k' },
{ label: 'Timeline', value: '2-3-months' },
])
expect(templateData.attachments).toEqual([
{ filename: 'brief.pdf', sizeLabel: '1.21 KB' },
])
expect(templateData.attachments).toEqual([{ filename: 'brief.pdf', sizeLabel: '1.21 KB' }])
expect(templateData.consentGiven).toBe('Yes')
expect(templateData.messageHtml).toContain('&lt;ASAP&gt;')
expect(templateData.messageHtml).toContain('&amp;')
Expand Down
19 changes: 14 additions & 5 deletions src/actions/contact/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import {
getResendApiKey,
isProd,
} from '@actions/utils/environment/environmentActions'
import { ActionsFunctionError, handleActionsFunctionError, throwActionError } from '@actions/utils/errors'
import {
ActionsFunctionError,
handleActionsFunctionError,
throwActionError,
} from '@actions/utils/errors'
import { contactFormSender, contactInbox, contactReplyTo } from '@actions/utils/email/resendSenders'
import { createConsentRecord } from '@actions/gdpr/entities/consent'
import { createOrUpdateContact, setMarketingOptIn } from '@actions/utils/hubspot'
Expand Down Expand Up @@ -121,7 +125,8 @@ export const contact = {
userAgent,
ipAddress: ip !== 'unknown' ? ip : null,
privacyPolicyVersion: getPrivacyPolicyVersion(),
consentText: null,
consentText:
'I consent to Webstack Builders processing my personal data for responding to your inquiry. See our Privacy Policy and Cookie Policy.',
verified: true,
})
}
Expand Down Expand Up @@ -189,9 +194,13 @@ export const contact = {
throw error
}

throwActionError(error, { route, operation: 'submit' }, {
fallbackMessage: 'Failed to send email. Please try again later.',
})
throwActionError(
error,
{ route, operation: 'submit' },
{
fallbackMessage: 'Failed to send email. Please try again later.',
}
)
}
},
}),
Expand Down
21 changes: 9 additions & 12 deletions src/actions/contact/utils.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import { z } from 'astro/zod'
import type {
ContactTimeline,
RequiredStringOptions,
} from '@actions/contact/@types'
import type { ContactTimeline, RequiredStringOptions } from '@actions/contact/@types'
import { isAllowedTimeline } from './responder'

const requiredStringError = (
requiredMessage: string,
invalidTypeMessage: string
) => (issue: { input?: unknown }): string =>
issue.input === undefined ? requiredMessage : invalidTypeMessage
const requiredStringError =
(requiredMessage: string, invalidTypeMessage: string) =>
(issue: { input?: unknown }): string =>
issue.input === undefined ? requiredMessage : invalidTypeMessage

export function escapeHtml(text: string): string {
const map: Record<string, string> = {
Expand Down Expand Up @@ -39,7 +35,8 @@ export function readString(form: FormData, key: string): string {
return typeof value === 'string' ? value : ''
}

export const trimString = (value: unknown): unknown => (typeof value === 'string' ? value.trim() : value)
export const trimString = (value: unknown): unknown =>
typeof value === 'string' ? value.trim() : value

export const emptyStringToUndefined = (value: unknown): unknown => {
if (value === null) return undefined
Expand All @@ -66,8 +63,8 @@ export const requiredString = (options: RequiredStringOptions) => {
)
}

export const isFile = (value: unknown): value is File => typeof File !== 'undefined' && value instanceof File

export const isFile = (value: unknown): value is File =>
typeof File !== 'undefined' && value instanceof File

export const optionalFile = () => z.custom<File>(isFile).optional()

Expand Down
19 changes: 12 additions & 7 deletions src/actions/downloads/__tests__/action.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ type ActionConfig<Input, Output> = {
handler: (_input: Input, _context: unknown) => Promise<Output>
}

const getMockedHandler = <Input, Output>(action: unknown): ActionConfig<Input, Output>['handler'] => {
const getMockedHandler = <Input, Output>(
action: unknown
): ActionConfig<Input, Output>['handler'] => {
return (action as ActionConfig<Input, Output>).handler
}

Expand Down Expand Up @@ -95,11 +97,14 @@ describe('downloads.submit.handler', () => {
clientAddress: '203.0.113.10',
}

const response = await getMockedHandler(downloads.submit)({
firstName: 'Jane',
lastName: 'Doe',
workEmail: 'jane@example.com',
}, context)
const response = await getMockedHandler(downloads.submit)(
{
firstName: 'Jane',
lastName: 'Doe',
workEmail: 'jane@example.com',
},
context
)

expect(response).toEqual({
success: true,
Expand All @@ -112,4 +117,4 @@ describe('downloads.submit.handler', () => {
})
expect(createConsentRecord).not.toHaveBeenCalled()
})
})
})
3 changes: 2 additions & 1 deletion src/actions/downloads/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ export const downloads = {
userAgent,
ipAddress: ip !== 'unknown' ? ip : null,
privacyPolicyVersion: getPrivacyPolicyVersion(),
consentText: null,
consentText:
'I consent to Webstack Builders processing my personal data for providing your requested download. See our Privacy Policy and Cookie Policy.',
verified: true,
})
}
Expand Down
5 changes: 3 additions & 2 deletions src/actions/gdpr/__tests__/responder.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { describe, expect, it, vi } from 'vitest'

vi.mock('@actions/utils/environment/environmentActions', async (importOriginal) => {
const actual = (await importOriginal()) as typeof import('@actions/utils/environment/environmentActions')
vi.mock('@actions/utils/environment/environmentActions', async importOriginal => {
const actual =
(await importOriginal()) as typeof import('@actions/utils/environment/environmentActions')
return {
...actual,
getPrivacyPolicyVersion: () => 'test-privacy-policy-version',
Expand Down
20 changes: 4 additions & 16 deletions src/actions/gdpr/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,9 @@ import emailValidator from 'email-validator'
import { validate as uuidValidate } from 'uuid'
import { defineAction } from 'astro:actions'
import { z } from 'astro/zod'
import {
checkRateLimit,
rateLimiters
} from '@actions/utils/rateLimit'
import {
buildRequestFingerprint,
createRateLimitIdentifier
} from '@actions/utils/requestContext'
import {
ActionsFunctionError,
handleActionsFunctionError
} from '@actions/utils/errors'
import { checkRateLimit, rateLimiters } from '@actions/utils/rateLimit'
import { buildRequestFingerprint, createRateLimitIdentifier } from '@actions/utils/requestContext'
import { ActionsFunctionError, handleActionsFunctionError } from '@actions/utils/errors'
import type {
ConsentResponse,
DSARRequestInput,
Expand All @@ -26,10 +17,7 @@ import {
consentDeleteSchema,
dsarRequestSchema,
} from '@actions/gdpr/domain'
import {
buildRateLimitError,
mapConsentRecord,
} from '@actions/gdpr/responder'
import { buildRateLimitError, mapConsentRecord } from '@actions/gdpr/responder'
import {
createConsentRecord,
createConsentRecordInput,
Expand Down
8 changes: 7 additions & 1 deletion src/actions/gdpr/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
export const CONSENT_PURPOSES = ['contact', 'marketing', 'analytics', 'functional', 'downloads'] as const
export const CONSENT_PURPOSES = [
'contact',
'marketing',
'analytics',
'functional',
'downloads',
] as const

export type ConsentPurpose = (typeof CONSENT_PURPOSES)[number]

Expand Down
2 changes: 1 addition & 1 deletion src/actions/gdpr/email/dsarText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ Questions? Contact us at ${company.dataProtectionOfficer.email}

© ${new Date().getFullYear()} ${company.name}. All rights reserved.
`.trim()
}
}
2 changes: 1 addition & 1 deletion src/actions/gdpr/entities/consent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {
ConsentEventRecord,
ConsentRequest,
CreateConsentRecordInput,
DbConsentRecord
DbConsentRecord,
} from '@actions/gdpr/@types'
import {
normalizeNullableString,
Expand Down
Loading
Loading