-
Notifications
You must be signed in to change notification settings - Fork 632
/
Copy pathcontact.tsx
230 lines (215 loc) · 6.59 KB
/
contact.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
import {
type ActionFunctionArgs,
json,
type HeadersFunction,
type MetaFunction,
} from '@remix-run/node'
import { Link, useFetcher } from '@remix-run/react'
import { Button } from '#app/components/button.tsx'
import {
ButtonGroup,
ErrorPanel,
Field,
} from '#app/components/form-elements.tsx'
import { Grid } from '#app/components/grid.tsx'
import {
HeroSection,
getHeroImageProps,
} from '#app/components/sections/hero-section.tsx'
import { H2, Paragraph } from '#app/components/typography.tsx'
import { getGenericSocialImage, images } from '#app/images.tsx'
import { type RootLoaderType } from '#app/root.tsx'
import { handleFormSubmission } from '#app/utils/actions.server.ts'
import { getDisplayUrl, getUrl } from '#app/utils/misc.tsx'
import { sendEmail } from '#app/utils/send-email.server.ts'
import { getSocialMetas } from '#app/utils/seo.ts'
import { requireUser } from '#app/utils/session.server.ts'
import { useRootData } from '#app/utils/use-root-data.ts'
function getErrorForSubject(subject: string | null) {
if (!subject) return `Subject is required`
if (subject.length <= 5) return `Subject is too short`
if (subject.length > 120) return `Subject is too long`
return null
}
function getErrorForBody(body: string | null) {
if (!body) return `Body is required`
if (body.length <= 40) return `Body is too short`
if (body.length > 1001) return `Body is too long`
return null
}
type ActionData = {
status: 'success' | 'error'
fields: {
subject?: string | null
body?: string | null
}
errors: {
generalError?: string
subject?: string | null
body?: string | null
}
}
export async function action({ request }: ActionFunctionArgs) {
const user = await requireUser(request)
return handleFormSubmission<ActionData>({
request,
validators: {
subject: getErrorForSubject,
body: getErrorForBody,
},
handleFormValues: async (fields) => {
const { subject, body } = fields
const sender = `"${user.firstName}" <${user.email}>`
// this bit is included so I can have a filter that ensures
// messages sent from the contact form never end up in spam.
const noSpamMessage = '- Sent via the KCD Contact Form'
await sendEmail({
from: sender,
to: `"Kent C. Dodds" <[email protected]>`,
subject,
text: `${body}\n\n${noSpamMessage}`,
})
const actionData: ActionData = { fields, status: 'success', errors: {} }
return json(actionData)
},
})
}
export const headers: HeadersFunction = () => ({
'Cache-Control': 'private, max-age=3600',
Vary: 'Cookie',
})
export const meta: MetaFunction<{}, { root: RootLoaderType }> = ({
matches,
}) => {
const requestInfo = matches.find((m) => m.id === 'root')?.data.requestInfo
return getSocialMetas({
title: 'Contact Kent C. Dodds',
description: 'Send Kent C. Dodds a personal email.',
url: getUrl(requestInfo),
image: getGenericSocialImage({
url: getDisplayUrl(requestInfo),
featuredImage: 'unsplash/photo-1563225409-127c18758bd5',
words: `Shoot Kent an email`,
}),
})
}
export default function ContactRoute() {
const contactFetcher = useFetcher<typeof action>()
const { user } = useRootData()
const isDone = contactFetcher.state === 'idle' && contactFetcher.data != null
const emailSuccessfullySent =
isDone && (contactFetcher.data as ActionData).status === 'success'
return (
<div>
<HeroSection
title="Send me an email."
subtitle="Like in the old days."
image={
<img
{...getHeroImageProps(images.kentProfile, {
className:
'max-h-50vh rounded-bl-3xl rounded-br-[25%] rounded-tl-[25%] rounded-tr-3xl',
})}
/>
}
/>
<main>
<contactFetcher.Form
method="POST"
noValidate
aria-describedby="contact-form-error"
>
<Grid>
<div className="col-span-full mb-12 lg:col-span-8 lg:col-start-3">
<H2>Email me</H2>
<Paragraph>
{`
I do my best to respond, but unfortunately I can't always
respond to every email I receive. If you have a support
request about my open source work, please open an issue
on the GitHub repo instead. If you have a support need on one of
my courses, please email the team (`}
<a href="mailto:[email protected]">[email protected]</a>
{`, `}
<a href="mailto:[email protected]">
</a>
{`, or `}
<a href="mailto:[email protected]">[email protected]</a>
{`) instead. I'll just forward your message to them anyway.`}
</Paragraph>
</div>
<div className="col-span-full lg:col-span-8 lg:col-start-3">
{user ? (
<>
<Field
name="name"
label="Name"
placeholder="Your name"
disabled={true}
defaultValue={user.firstName}
/>
<Field
type="email"
label="Email"
placeholder="[email protected]"
disabled={true}
defaultValue={user.email}
name="email"
/>
<Field
name="subject"
label="Subject"
placeholder="No subject"
defaultValue={contactFetcher.data?.fields.subject ?? ''}
error={contactFetcher.data?.errors.subject}
/>
<Field
name="body"
label="Body"
type="textarea"
placeholder="A clear and concise message works wonders."
rows={8}
defaultValue={contactFetcher.data?.fields.body ?? ''}
error={contactFetcher.data?.errors.body}
/>
{emailSuccessfullySent ? (
`Hooray, email sent! 🎉`
) : (
// IDEA: show a loading state here
<ButtonGroup>
<Button
type="submit"
disabled={contactFetcher.state !== 'idle'}
>
Send message
</Button>
<Button variant="secondary" type="reset">
Reset form
</Button>
</ButtonGroup>
)}
{contactFetcher.data?.errors.generalError ? (
<ErrorPanel id="contact-form-error">
{contactFetcher.data.errors.generalError}
</ErrorPanel>
) : null}
</>
) : (
<div className="col-span-full mb-12 lg:col-span-8 lg:col-start-3">
<Paragraph>
Note: due to spam issues, you have to confirm your email by{' '}
<Link to="/login" className="underline">
signing up for an account
</Link>{' '}
on my website first.
</Paragraph>
</div>
)}
</div>
</Grid>
</contactFetcher.Form>
</main>
</div>
)
}