|
| 1 | +import { ActionArgs, json } from "@remix-run/node"; |
| 2 | +import { Form, Link, useActionData, useNavigation } from "@remix-run/react"; |
| 3 | +import * as z from "zod"; |
| 4 | + |
| 5 | +import { |
| 6 | + calculateDaysUntilNextBirthday, |
| 7 | + isDateFormat, |
| 8 | +} from "~/lib/utils.server"; |
| 9 | + |
| 10 | +export const action = async ({ request }: ActionArgs) => { |
| 11 | + const formData = await request.formData(); |
| 12 | + const payload = Object.fromEntries(formData.entries()); |
| 13 | + |
| 14 | + const currentDate = new Date(); |
| 15 | + |
| 16 | + const schema = z.object({ |
| 17 | + firstName: z |
| 18 | + .string() |
| 19 | + .min(2, "Must be at least 2 characters") |
| 20 | + .max(50, "Must be less than 50 characters"), |
| 21 | + email: z.string().email("Must be a valid email"), |
| 22 | + birthday: z.coerce |
| 23 | + .date() |
| 24 | + .min( |
| 25 | + new Date( |
| 26 | + currentDate.getFullYear() - 26, |
| 27 | + currentDate.getMonth(), |
| 28 | + currentDate.getDate(), |
| 29 | + ), |
| 30 | + "Must be at younger than 25", |
| 31 | + ) |
| 32 | + .max( |
| 33 | + new Date( |
| 34 | + currentDate.getFullYear() - 18, |
| 35 | + currentDate.getMonth(), |
| 36 | + currentDate.getDate(), |
| 37 | + ), |
| 38 | + "Must be at least 18 years old", |
| 39 | + ), |
| 40 | + }); |
| 41 | + |
| 42 | + const parseResult = schema.safeParse(payload); |
| 43 | + |
| 44 | + if (!parseResult.success) { |
| 45 | + const fields = { |
| 46 | + firstName: typeof payload.firstName === "string" ? payload.firstName : "", |
| 47 | + email: typeof payload.email === "string" ? payload.email : "", |
| 48 | + birthday: |
| 49 | + typeof payload.birthday === "string" && isDateFormat(payload.birthday) |
| 50 | + ? payload.birthday |
| 51 | + : "", |
| 52 | + }; |
| 53 | + |
| 54 | + return json( |
| 55 | + { |
| 56 | + fieldErrors: parseResult.error.flatten().fieldErrors, |
| 57 | + fields, |
| 58 | + message: null, |
| 59 | + }, |
| 60 | + { |
| 61 | + status: 400, |
| 62 | + }, |
| 63 | + ); |
| 64 | + } |
| 65 | + |
| 66 | + return json({ |
| 67 | + fieldErrors: null, |
| 68 | + fields: null, |
| 69 | + message: `Hello ${parseResult.data.firstName}! We will send an email to ${ |
| 70 | + parseResult.data.email |
| 71 | + } for your discount code in ${calculateDaysUntilNextBirthday( |
| 72 | + parseResult.data.birthday, |
| 73 | + )} days.`, |
| 74 | + }); |
| 75 | +}; |
| 76 | + |
| 77 | +const errorTextStyle: React.CSSProperties = { |
| 78 | + fontWeight: "bold", |
| 79 | + color: "red", |
| 80 | + marginInline: 0, |
| 81 | + marginBlock: "0.25rem", |
| 82 | +}; |
| 83 | + |
| 84 | +export default function RegisterView() { |
| 85 | + const actionData = useActionData<typeof action>(); |
| 86 | + const navigation = useNavigation(); |
| 87 | + const isSubmitting = navigation.state === "submitting"; |
| 88 | + |
| 89 | + if (actionData?.message) { |
| 90 | + return ( |
| 91 | + <div> |
| 92 | + <h3>{actionData.message}</h3> |
| 93 | + <hr /> |
| 94 | + <Link to="/products">View Products</Link> |
| 95 | + </div> |
| 96 | + ); |
| 97 | + } |
| 98 | + |
| 99 | + return ( |
| 100 | + <div> |
| 101 | + <h1>Register for a birthday discount!</h1> |
| 102 | + <Form method="post"> |
| 103 | + <div> |
| 104 | + <label htmlFor="firstName">First Name:</label> |
| 105 | + <input |
| 106 | + type="text" |
| 107 | + id="firstName" |
| 108 | + name="firstName" |
| 109 | + defaultValue={actionData?.fields?.firstName} |
| 110 | + /> |
| 111 | + {actionData?.fieldErrors?.firstName |
| 112 | + ? actionData.fieldErrors.firstName.map((error, index) => ( |
| 113 | + <p style={errorTextStyle} key={`first-name-error-${index}`}> |
| 114 | + {error} |
| 115 | + </p> |
| 116 | + )) |
| 117 | + : null} |
| 118 | + </div> |
| 119 | + |
| 120 | + <br /> |
| 121 | + |
| 122 | + <div> |
| 123 | + <label htmlFor="email">Email:</label> |
| 124 | + <input |
| 125 | + type="email" |
| 126 | + id="email" |
| 127 | + name="email" |
| 128 | + defaultValue={actionData?.fields?.email} |
| 129 | + /> |
| 130 | + {actionData?.fieldErrors?.email |
| 131 | + ? actionData.fieldErrors.email.map((error, index) => ( |
| 132 | + <p style={errorTextStyle} key={`email-error-${index}`}> |
| 133 | + {error} |
| 134 | + </p> |
| 135 | + )) |
| 136 | + : null} |
| 137 | + </div> |
| 138 | + |
| 139 | + <br /> |
| 140 | + |
| 141 | + <div> |
| 142 | + <label htmlFor="birthday">Birthday:</label> |
| 143 | + <input |
| 144 | + type="date" |
| 145 | + id="birthday" |
| 146 | + name="birthday" |
| 147 | + defaultValue={actionData?.fields?.birthday} |
| 148 | + /> |
| 149 | + {actionData?.fieldErrors?.birthday |
| 150 | + ? actionData.fieldErrors.birthday.map((error, index) => ( |
| 151 | + <p style={errorTextStyle} key={`birthday-error-${index}`}> |
| 152 | + {error} |
| 153 | + </p> |
| 154 | + )) |
| 155 | + : null} |
| 156 | + </div> |
| 157 | + |
| 158 | + <br /> |
| 159 | + |
| 160 | + <button type="submit" disabled={isSubmitting}> |
| 161 | + Register |
| 162 | + </button> |
| 163 | + </Form> |
| 164 | + <hr /> |
| 165 | + <Link to="/products" prefetch="intent"> |
| 166 | + View Products |
| 167 | + </Link> |
| 168 | + </div> |
| 169 | + ); |
| 170 | +} |
0 commit comments