diff --git a/apps/website/gatsby-node.mjs b/apps/website/gatsby-node.mjs index aaaf21162..b3b3a79c1 100644 --- a/apps/website/gatsby-node.mjs +++ b/apps/website/gatsby-node.mjs @@ -102,6 +102,14 @@ export const createPages = async ({ actions }) => { }); }); + // Create a contact page in each language. + Object.values(Locale).forEach((locale) => { + actions.createPage({ + path: `/${locale}/contact`, + component: resolve(`./src/templates/contact.tsx`), + }); + }); + // Broken Gatsby links will attempt to load page-data.json files, which don't exist // and also should not be piped into the strangler function. Thats why they // are caught right here. diff --git a/apps/website/src/templates/contact.tsx b/apps/website/src/templates/contact.tsx new file mode 100644 index 000000000..77f8e781b --- /dev/null +++ b/apps/website/src/templates/contact.tsx @@ -0,0 +1,6 @@ +import { Contact } from '@custom/ui/routes/Contact'; +import React from 'react'; + +export default function ContentHubPage() { + return +} diff --git a/apps/website/src/utils/drupal-executor.ts b/apps/website/src/utils/drupal-executor.ts index a892e7a6e..0dfc1ad6d 100644 --- a/apps/website/src/utils/drupal-executor.ts +++ b/apps/website/src/utils/drupal-executor.ts @@ -9,23 +9,52 @@ export function drupalExecutor(endpoint: string, forward: boolean = true) { variables?: OperationVariables, ) { const url = new URL(endpoint, window.location.origin); - url.searchParams.set('queryId', id); - url.searchParams.set('variables', JSON.stringify(variables)); - const { data, errors } = await ( - await fetch(url, { - credentials: 'include', - headers: forward - ? { - 'SLB-Forwarded-Proto': window.location.protocol.slice(0, -1), - 'SLB-Forwarded-Host': window.location.hostname, - 'SLB-Forwarded-Port': window.location.port, - } - : {}, - }) - ).json(); - if (errors) { - throw errors; + if (variables && variables.graphqlOperationType === 'mutation') { + const { data, errors } = await ( + await fetch(url, { + method: 'POST', + credentials: 'include', + body: JSON.stringify({ + 'queryId': id, + 'variables': variables.variables, + }), + headers: forward + ? { + 'SLB-Forwarded-Proto': window.location.protocol.slice(0, -1), + 'SLB-Forwarded-Host': window.location.hostname, + 'SLB-Forwarded-Port': window.location.port, + "Content-Type": "application/json", + } + : { + "Content-Type": "application/json", + }, + }) + ).json(); + if (errors) { + throw errors; + } + return data; + } else { + url.searchParams.set('queryId', id); + if (variables?.variables) { + url.searchParams.set('variables', JSON.stringify(variables.variables)); + } + const { data, errors } = await ( + await fetch(url, { + credentials: 'include', + headers: forward + ? { + 'SLB-Forwarded-Proto': window.location.protocol.slice(0, -1), + 'SLB-Forwarded-Host': window.location.hostname, + 'SLB-Forwarded-Port': window.location.port, + } + : {}, + }) + ).json(); + if (errors) { + throw errors; + } + return data; } - return data; }; } diff --git a/packages/schema/src/operations/ContactRequest.gql b/packages/schema/src/operations/ContactRequest.gql new file mode 100644 index 000000000..8cf6c09e6 --- /dev/null +++ b/packages/schema/src/operations/ContactRequest.gql @@ -0,0 +1,17 @@ +mutation ContactRequest ( + $contact: ContactInput +){ + createContact(contact: $contact) { + errors { + key + field + message + } + contact { + name + email + message + subject + } + } +} \ No newline at end of file diff --git a/packages/ui/src/components/Molecules/ContactForm.stories.ts b/packages/ui/src/components/Molecules/ContactForm.stories.ts new file mode 100644 index 000000000..cb7511a9f --- /dev/null +++ b/packages/ui/src/components/Molecules/ContactForm.stories.ts @@ -0,0 +1,9 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { ContactForm as Component } from './ContactForm'; + +export default { + component: Component, +} satisfies Meta; + +export const Empty = {} satisfies StoryObj; diff --git a/packages/ui/src/components/Molecules/ContactForm.tsx b/packages/ui/src/components/Molecules/ContactForm.tsx new file mode 100644 index 000000000..e5e8f52d6 --- /dev/null +++ b/packages/ui/src/components/Molecules/ContactForm.tsx @@ -0,0 +1,118 @@ +import { ContactRequestMutation } from '@custom/schema'; +import React from 'react'; +import { useForm } from 'react-hook-form'; +import { useIntl } from 'react-intl'; +import { z } from 'zod'; + +import { useMutation } from '../../utils/operation'; +import { Messages } from './Messages'; + +const formValueSchema = z.object({ + name: z.string(), + email: z.string(), + subject: z.string().optional(), + message: z.string(), +}); + +export function ContactForm() { + const intl = useIntl(); + type FormValue = z.infer; + const { register, handleSubmit } = useForm(); + + const { data, trigger, isMutating } = useMutation(ContactRequestMutation); + const errorMessages = (!isMutating && data && data.createContact?.errors && data.createContact.errors.length > 0 ) ? + data.createContact.errors.map(error => { + return error?.message || ''; + }) + : + null; + return ( + + { errorMessages ? : null} + { + // @todo: fix this + // @ts-ignore + trigger({ + contact: values, + }); + })} + > + + + {intl.formatMessage({ + defaultMessage: 'Name', + id: 'HAlOn1', + })} + + + + + + {intl.formatMessage({ + defaultMessage: 'Email', + id: 'sy+pv5', + })} + + + + + + {intl.formatMessage({ + defaultMessage: 'Subject', + id: 'LLtKhp', + })} + + + + + + {intl.formatMessage({ + defaultMessage: 'Message', + id: 'T7Ry38', + })} + + + + + + {isMutating ? intl.formatMessage({ defaultMessage: 'Sending...', id: '82Y7Sa' }) : intl.formatMessage({ defaultMessage: 'Submit', id: 'wSZR47' })} + + + + + ); +} diff --git a/packages/ui/src/components/Organisms/Contact.tsx b/packages/ui/src/components/Organisms/Contact.tsx new file mode 100644 index 000000000..22a40d0ab --- /dev/null +++ b/packages/ui/src/components/Organisms/Contact.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +import { ContactForm } from '../Molecules/ContactForm'; + +export function Contact() { + return ( + + + + ); +} diff --git a/packages/ui/src/components/Routes/Contact.tsx b/packages/ui/src/components/Routes/Contact.tsx new file mode 100644 index 000000000..1a3963ced --- /dev/null +++ b/packages/ui/src/components/Routes/Contact.tsx @@ -0,0 +1,20 @@ +import { Locale } from '@custom/schema'; +import React from 'react'; + +import { useTranslations } from '../../utils/translations'; +import { PageTransition } from '../Molecules/PageTransition'; +import { Contact as ContactOrganism } from '../Organisms/Contact'; + +export function Contact() { + // Initialize the contact page in each language. + useTranslations( + Object.fromEntries( + Object.values(Locale).map((locale) => [locale, `/${locale}/contact`]), + ), + ); + return ( + + + + ); +} diff --git a/packages/ui/src/utils/operation.ts b/packages/ui/src/utils/operation.ts index 7b84ce3fe..e4c0460b0 100644 --- a/packages/ui/src/utils/operation.ts +++ b/packages/ui/src/utils/operation.ts @@ -5,30 +5,65 @@ import { OperationVariables, } from '@custom/schema'; import useSwr, { SWRResponse } from 'swr'; +import useSWRMutation, { SWRMutationResponse } from 'swr/mutation'; + +function swrFetcher( + operationMetadata: { + operation: TOperation, + variables?: OperationVariables, + }, +) { + const executor = createExecutor(operationMetadata.operation, { + variables: operationMetadata.variables, + }); + if (typeof executor === 'function') { + // @todo: fix this. + // @ts-ignore + return executor(); + } + // If the executor is not a function, then just return it. This means the + // executor is already the data we want. + return executor; +} + +function swrMutator( + operationMetadata: { + operation: TOperation, + }, + args?: OperationVariables, +) { + const executor = createExecutor(operationMetadata.operation, { + graphqlOperationType: 'mutation', + variables: args?.arg, + }); + if (typeof executor === 'function') { + // @todo: fix this. + // @ts-ignore + return executor(); + } + return executor; +} export function useOperation( operation: TOperation, variables?: OperationVariables, -): Omit>, 'mutate'> { - const executor = createExecutor(operation, variables); - // If the executor is a function, use SWR to manage it. - const result = useSwr>( - [operation, variables], - // If the executor is not a function, pass null to SWR, - // so it does not try to fetch. - typeof executor === 'function' ? executor : null, +): SWRResponse> { + return useSwr>( + {operation, variables}, + swrFetcher, { suspense: false, }, ); +} - return typeof executor === 'function' - ? result - : // If the executor is not a function, return a mock SWR response. - { - data: executor, - error: undefined, - isValidating: false, - isLoading: false, - }; +export function useMutation( + operation: TOperation, +): SWRMutationResponse> { + return useSWRMutation>( + {operation}, + // @todo: fix this. + // @ts-ignore + swrMutator, + ); } diff --git a/tests/schema/specs/contact.spec.ts b/tests/schema/specs/contact.spec.ts index fdb7fca24..b207a5e85 100644 --- a/tests/schema/specs/contact.spec.ts +++ b/tests/schema/specs/contact.spec.ts @@ -66,7 +66,7 @@ describe('create contact', () => { { "field": "email", "key": "invalid_field_email", - "message": "The email address invalid_email is not valid. Use the format user@example.com.", + "message": "The email address invalid_email is not valid. Use the format user@example.com.", }, ], },