Skip to content

Commit af16309

Browse files
authored
✨ feat: Add contact_us in nextjs (#150)
1 parent 4309ab5 commit af16309

File tree

7 files changed

+218
-8
lines changed

7 files changed

+218
-8
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
'use client'
2+
import { getFormProps, useForm } from '@conform-to/react'
3+
import { getZodConstraint, parseWithZod } from '@conform-to/zod'
4+
import { CheckCircle2, CircleAlert } from 'lucide-react'
5+
import { Input, Textarea } from '@/components/form'
6+
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
7+
import { Button } from '@/components/ui/button'
8+
import { Label } from '@/components/ui/label'
9+
import { contactFormSchema } from '@/integration/forms/ContactForm/schema'
10+
import { useActionState } from 'react'
11+
import { subitContactFormAction } from '@/integration/forms/ContactForm/action'
12+
13+
export const ContactForm = () => {
14+
const [state, action] = useActionState(subitContactFormAction, undefined)
15+
const [form, fields] = useForm({
16+
id: 'contact-form',
17+
lastResult: state?.reply,
18+
constraint: getZodConstraint(contactFormSchema),
19+
onValidate({ formData }) {
20+
return parseWithZod(formData, { schema: contactFormSchema })
21+
},
22+
shouldValidate: 'onBlur',
23+
})
24+
25+
return (
26+
<div className="flex items-center justify-center">
27+
<div className="container mx-auto max-w-xl">
28+
{state?.reply.status === 'success' ? (
29+
<div className="space-y-4">
30+
<Alert>
31+
<CheckCircle2 className="stroke-green-500" />
32+
<AlertTitle
33+
className="text-lg"
34+
dangerouslySetInnerHTML={{
35+
__html: '<h2>Success!</h2>',
36+
}}
37+
></AlertTitle>
38+
<AlertDescription
39+
dangerouslySetInnerHTML={{
40+
__html: '<p>Your message has been sent successfully.</p>',
41+
}}
42+
></AlertDescription>
43+
</Alert>
44+
</div>
45+
) : (
46+
<form
47+
{...getFormProps(form)}
48+
method="POST"
49+
action={action}
50+
className="space-y-4"
51+
>
52+
{form.errors && (
53+
<div className="space-y-4">
54+
<Alert>
55+
<CircleAlert className="stroke-red-500" />
56+
<AlertTitle className="text-lg">Form Error!</AlertTitle>
57+
<AlertDescription>{form.errors}</AlertDescription>
58+
</Alert>
59+
</div>
60+
)}
61+
<div className="space-y-2">
62+
<Label htmlFor={fields.name.id}>Name</Label>
63+
<Input
64+
meta={fields.name}
65+
type="text"
66+
placeholder="Enter your name"
67+
/>
68+
{fields.name.errors && (
69+
<p className="text-sm text-red-500">{fields.name.errors}</p>
70+
)}
71+
</div>
72+
73+
<div className="space-y-2">
74+
<Label htmlFor={fields.email.id}>Email</Label>
75+
<Input
76+
meta={fields.email}
77+
type="email"
78+
placeholder="Enter your email"
79+
/>
80+
{fields.email.errors && (
81+
<p className="text-sm text-red-500">{fields.email.errors}</p>
82+
)}
83+
</div>
84+
85+
<div className="space-y-2">
86+
<Label htmlFor={fields.message.id}>Message</Label>
87+
<Textarea
88+
meta={fields.message}
89+
placeholder="Enter your message"
90+
className="min-h-[120px]"
91+
/>
92+
{fields.message.errors && (
93+
<p className="text-sm text-red-500">{fields.message.errors}</p>
94+
)}
95+
</div>
96+
<Button type="submit" className="w-full">
97+
Submit
98+
</Button>
99+
</form>
100+
)}
101+
</div>
102+
</div>
103+
)
104+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use server'
2+
import { parseWithZod } from '@conform-to/zod'
3+
import { contactFormSchema } from './schema'
4+
import { submitContactFormFunction } from '@/integration/forms/ContactForm/function'
5+
6+
export async function submitContactFormAction(
7+
_previousState: unknown,
8+
formData: FormData
9+
) {
10+
const submission = parseWithZod(formData, {
11+
schema: contactFormSchema,
12+
})
13+
14+
if (submission.status !== 'success') {
15+
return {
16+
reply: submission.reply(),
17+
data: null,
18+
}
19+
}
20+
21+
const result = await submitContactFormFunction(submission.value)
22+
23+
if (!result.success) {
24+
return {
25+
reply: submission.reply({
26+
formErrors: [
27+
'There was an error submitting the form. Please try again later.',
28+
],
29+
}),
30+
data: null,
31+
}
32+
}
33+
34+
return {
35+
reply: submission.reply(),
36+
data: result.data,
37+
}
38+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { graphql } from '@/graphql/gql.tada'
2+
import { getClient } from '@/utils/client'
3+
import { composable } from 'composable-functions'
4+
import { ContactFormSchema } from '@/integration/forms/ContactForm/schema'
5+
6+
const contactMutation = graphql(`
7+
mutation SubmitContactForm($input: [KeyValueInput]) {
8+
submitWebform(id: "contact_form", data: $input) {
9+
confirmation {
10+
confirmation_title
11+
confirmation_message
12+
}
13+
}
14+
}
15+
`)
16+
17+
async function submitContactForm(input: ContactFormSchema) {
18+
const client = await getClient({
19+
url: process.env.DRUPAL_GRAPHQL_URI!,
20+
auth: {
21+
uri: process.env.DRUPAL_AUTH_URI!,
22+
clientId: process.env.DRUPAL_CLIENT_ID!,
23+
clientSecret: process.env.DRUPAL_CLIENT_SECRET!,
24+
},
25+
})
26+
27+
const inputArray = Object.entries(input).map(([key, value]) => ({
28+
key,
29+
value,
30+
}))
31+
const result = await client.mutation(contactMutation, { input: inputArray })
32+
33+
if (!result.data?.submitWebform?.confirmation) {
34+
throw new Error('Error submitting contact form')
35+
}
36+
37+
return result.data.submitWebform.confirmation
38+
}
39+
40+
export const submitContactFormFunction = composable(submitContactForm)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { z } from 'zod'
2+
3+
export const contactFormSchema = z.object({
4+
name: z.string().min(1, 'Name is required'),
5+
email: z.string().email('Invalid email address').min(1, 'Email is required'),
6+
message: z.string().optional(),
7+
})
8+
9+
export type ContactFormSchema = z.infer<typeof contactFormSchema>

starters/next/integration/resolvers/ParagraphWebformResolver.tsx

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { FragmentOf, readFragment } from 'gql.tada'
2+
import { ContactForm } from '@/integration/forms/ContactForm/ContactForm'
23

34
import { WebformFragment } from '@/graphql/fragments/webform'
45
import { graphql } from '@/graphql/gql.tada'
@@ -28,18 +29,30 @@ export const ParagraphWebformFragment = graphql(
2829
export const ParagraphWebformResolver = ({
2930
paragraph,
3031
}: ParagraphWebformProps) => {
31-
const { id, heading, subheadingOptional, descriptionOptional, form } =
32+
const { heading, subheadingOptional, descriptionOptional, form } =
3233
readFragment(ParagraphWebformFragment, paragraph)
3334

3435
return (
35-
<div className="container mx-auto grid items-center gap-8 pt-8 pb-8 lg:grid-cols-2">
36-
<pre>
37-
{JSON.stringify(
38-
{ id, descriptionOptional, heading, subheadingOptional, form },
39-
null,
40-
2
36+
<div className="container mx-auto py-8 md:py-16 lg:py-24">
37+
<div>
38+
<h2 className="mb-5 text-3xl font-bold sm:text-4xl md:text-5xl">
39+
{heading}
40+
</h2>
41+
{subheadingOptional && (
42+
<h3 className="mb-3 text-xl">{subheadingOptional}</h3>
4143
)}
42-
</pre>
44+
{descriptionOptional && (
45+
<p
46+
className="text-muted-foreground mb-5 text-lg"
47+
dangerouslySetInnerHTML={{ __html: descriptionOptional }}
48+
/>
49+
)}
50+
{form && (
51+
<div className="py-8 md:py-16 lg:py-24">
52+
<ContactForm />
53+
</div>
54+
)}
55+
</div>
4356
</div>
4457
)
4558
}

starters/next/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"@urql/core": "^5.0.1",
2626
"class-variance-authority": "^0.7.1",
2727
"clsx": "^2.1.1",
28+
"composable-functions": "^5.0.0",
2829
"drupal-auth-client": "^0.4",
2930
"drupal-decoupled": "^0.2.2",
3031
"gql.tada": "^1.8.10",

starters/next/yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3554,6 +3554,11 @@ commondir@^1.0.1:
35543554
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
35553555
integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==
35563556

3557+
composable-functions@^5.0.0:
3558+
version "5.0.0"
3559+
resolved "https://registry.yarnpkg.com/composable-functions/-/composable-functions-5.0.0.tgz#1dbe89a20779ec24fc7d8e2bdcea0b9644948b72"
3560+
integrity sha512-CV5r9eXpnombeKTHFP895n++aYIJkmpHrlDXqloF0OUYdrXr7CU008R8tcBjaDBcZHrSwB03W1Lv8T8Ly8C37A==
3561+
35573562
35583563
version "0.0.1"
35593564
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"

0 commit comments

Comments
 (0)