a tool for shared writing and social publishing
1"use client";
2import { useSmoker, useToaster } from "components/Toast";
3import { RSVP_Status, RSVPButtons, State, useRSVPNameState } from ".";
4import { createContext, useContext, useState } from "react";
5import { useRSVPData } from "components/PageSWRDataProvider";
6import { confirmPhoneAuthToken } from "actions/phone_auth/confirm_phone_auth_token";
7import { submitRSVP } from "actions/phone_rsvp_to_event";
8
9import { countryCodes } from "src/constants/countryCodes";
10import { Checkbox } from "components/Checkbox";
11import { ButtonPrimary, ButtonTertiary } from "components/Buttons";
12import { Separator } from "components/Layout";
13import { createPhoneAuthToken } from "actions/phone_auth/request_phone_auth_token";
14import { Input, InputWithLabel } from "components/Input";
15import { RequestHeadersContext } from "components/Providers/RequestHeadersProvider";
16import { Popover } from "components/Popover";
17import { theme } from "tailwind.config";
18import { InfoSmall } from "components/Icons/InfoSmall";
19
20export function ContactDetailsForm(props: {
21 status: RSVP_Status;
22 entityID: string;
23 setState: (s: State) => void;
24 setStatus: (s: RSVP_Status) => void;
25}) {
26 let { status, entityID, setState, setStatus } = props;
27 let focusWithinStyles =
28 "focus-within:border-tertiary focus-within:outline-solid focus-within:outline-2 focus-within:outline-tertiary focus-within:outline-offset-1";
29 let toaster = useToaster();
30 let { data, mutate } = useRSVPData();
31 let [contactFormState, setContactFormState] = useState<
32 { state: "details" } | { state: "confirm"; token: string }
33 >({ state: "details" });
34 let { name, setName } = useRSVPNameState();
35 let [plus_ones, setPlusOnes] = useState(
36 data?.rsvps?.find(
37 (rsvp) =>
38 data.authToken &&
39 rsvp.entity === props.entityID &&
40 data.authToken.country_code === rsvp.country_code &&
41 data.authToken.phone_number === rsvp.phone_number,
42 )?.plus_ones || 0,
43 );
44 let requestHeaders = useContext(RequestHeadersContext);
45 const [formState, setFormState] = useState({
46 country_code:
47 countryCodes.find((c) => c[1].toUpperCase() === (requestHeaders.country || "US"))?.[2] || "1",
48 phone_number: "",
49 confirmationCode: "",
50 });
51
52 let submit = async (
53 token: Awaited<ReturnType<typeof confirmPhoneAuthToken>>,
54 ) => {
55 try {
56 await submitRSVP({
57 status,
58 name: name,
59 entity: entityID,
60 plus_ones,
61 });
62 } catch (e) {
63 //handle failed confirm
64 return false;
65 }
66
67 mutate({
68 authToken: token,
69 rsvps: [
70 ...(data?.rsvps || []).filter((r) => r.entity !== entityID),
71 {
72 name: name,
73 status,
74 plus_ones,
75 entity: entityID,
76 phone_number: token.phone_number,
77 country_code: token.country_code,
78 },
79 ],
80 });
81 props.setState({ state: "default" });
82 return true;
83 };
84 return contactFormState.state === "details" ? (
85 <>
86 <form
87 className="rsvpForm flex flex-col gap-2"
88 onSubmit={async (e) => {
89 e.preventDefault();
90 if (data?.authToken) {
91 submit(data.authToken);
92 toaster({
93 content: (
94 <div className="font-bold">
95 {status === "GOING"
96 ? "Yay! You're Going!"
97 : status === "MAYBE"
98 ? "You're a Maybe"
99 : "Sorry you can't make it D:"}
100 </div>
101 ),
102 type: "success",
103 });
104 } else {
105 let tokenId = await createPhoneAuthToken(formState);
106 setContactFormState({ state: "confirm", token: tokenId });
107 }
108 }}
109 >
110 <RSVPButtons setStatus={props.setStatus} status={props.status} />
111
112 <div className="rsvpInputs flex sm:flex-row flex-col gap-2 w-fit place-self-center ">
113 <label
114 htmlFor="rsvp-name-input"
115 className={`
116 rsvpNameInput input-with-border h-fit
117 flex flex-col ${focusWithinStyles}`}
118 >
119 <div className="text-xs font-bold italic text-tertiary">name</div>
120 <Input
121 autoFocus
122 id="rsvp-name-input"
123 placeholder="..."
124 className=" bg-transparent disabled:text-tertiary w-full appearance-none focus:outline-0"
125 value={name}
126 onKeyDown={(e) => {
127 if (e.key === "Backspace" && !e.currentTarget.value)
128 e.preventDefault();
129 }}
130 onChange={(e) => setName(e.target.value)}
131 />
132 </label>
133 <div
134 className={`rsvpPhoneInputWrapper relative flex flex-col gap-0.5 w-full basis-2/3`}
135 >
136 <label
137 htmlFor="rsvp-phone-input"
138 className={`
139 rsvpPhoneInput input-with-border
140 flex flex-col ${focusWithinStyles}
141 ${!!data?.authToken?.phone_number && "bg-border-light border-border-light text-tertiary"}`}
142 >
143 <div className=" text-xs font-bold italic text-tertiary">
144 Phone Number
145 </div>
146 <div className="flex gap-2 ">
147 <div className="flex items-center gap-1">
148 <span
149 style={{
150 color:
151 formState.country_code === "" ||
152 !!data?.authToken?.phone_number
153 ? theme.colors.tertiary
154 : theme.colors.primary,
155 }}
156 >
157 +
158 </span>
159 <Input
160 onKeyDown={(e) => {
161 if (e.key === "Backspace" && !e.currentTarget.value)
162 e.preventDefault();
163 }}
164 disabled={!!data?.authToken?.phone_number}
165 className="w-10 bg-transparent appearance-none focus:outline-0"
166 placeholder="1"
167 maxLength={4}
168 inputMode="numeric"
169 pattern="[0-9]*"
170 value={formState.country_code}
171 onChange={(e) =>
172 setFormState((s) => ({
173 ...s,
174 country_code: e.target.value.replace(/[^0-9]/g, ""),
175 }))
176 }
177 />
178 </div>
179 <Separator />
180
181 <Input
182 id="rsvp-phone-input"
183 inputMode="numeric"
184 placeholder="0000000000"
185 pattern="[0-9]*"
186 className=" bg-transparent disabled:text-tertiary w-full appearance-none focus:outline-0"
187 disabled={!!data?.authToken?.phone_number}
188 onKeyDown={(e) => {
189 if (e.key === "Backspace" && !e.currentTarget.value)
190 e.preventDefault();
191 }}
192 value={
193 data?.authToken?.phone_number || formState.phone_number
194 }
195 onChange={(e) =>
196 setFormState((state) => ({
197 ...state,
198 phone_number: e.target.value.replace(/[^0-9]/g, ""),
199 }))
200 }
201 />
202 </div>
203 </label>
204 <div className="text-xs italic text-tertiary leading-tight">
205 {formState.country_code !== "1" ? (
206 <>
207 Messages to non-US/Canada numbers will be sent via{" "}
208 <strong>WhatsApp</strong>
209 </>
210 ) : null}
211 </div>
212 </div>
213 <div className="flex flex-row gap-2 w-full sm:w-32 h-fit">
214 <InputWithLabel
215 className="appearance-none!"
216 placeholder="0"
217 label="Plus ones?"
218 type="number"
219 min={0}
220 max={4}
221 value={plus_ones}
222 onChange={(e) => setPlusOnes(parseInt(e.currentTarget.value))}
223 onKeyDown={(e) => {
224 if (e.key === "Backspace" && !e.currentTarget.value)
225 e.preventDefault();
226 }}
227 />
228 </div>
229 </div>
230
231 <hr className="border-border" />
232 <div className="flex flex-row gap-2 w-full items-center justify-end">
233 <ConsentPopover country_code={formState.country_code} />
234 <ButtonTertiary
235 onMouseDown={() => {
236 setState({ state: "default" });
237 }}
238 >
239 Back
240 </ButtonTertiary>
241 <ButtonPrimary
242 disabled={
243 (!data?.authToken?.phone_number &&
244 (!formState.phone_number || !formState.country_code)) ||
245 !name
246 }
247 className="place-self-end"
248 type="submit"
249 >
250 RSVP as{" "}
251 {status === "GOING"
252 ? "Going"
253 : status === "MAYBE"
254 ? "Maybe"
255 : "Can't Go"}
256 </ButtonPrimary>
257 </div>
258 </form>
259 </>
260 ) : (
261 <ConfirmationForm
262 country_code={formState.country_code}
263 phoneNumber={formState.phone_number}
264 token={contactFormState.token}
265 value={formState.confirmationCode}
266 submit={submit}
267 status={status}
268 onChange={(value) =>
269 setFormState((state) => ({ ...state, confirmationCode: value }))
270 }
271 />
272 );
273}
274
275const ConfirmationForm = (props: {
276 country_code: string;
277 phoneNumber: string;
278 value: string;
279 token: string;
280 status: RSVP_Status;
281 submit: (
282 token: Awaited<ReturnType<typeof confirmPhoneAuthToken>>,
283 ) => Promise<boolean>;
284 onChange: (v: string) => void;
285}) => {
286 let smoker = useSmoker();
287 let toaster = useToaster();
288 return (
289 <form
290 className="flex flex-col gap-3 w-full"
291 onSubmit={async (e) => {
292 e.preventDefault();
293 let rect = document
294 .getElementById("rsvp-code-confirm-button")
295 ?.getBoundingClientRect();
296 try {
297 let token = await confirmPhoneAuthToken(props.token, props.value);
298 props.submit(token);
299 toaster({
300 content: (
301 <div className="font-bold">
302 {props.status === "GOING"
303 ? "Yay! You're Going!"
304 : props.status === "MAYBE"
305 ? "You're a Maybe"
306 : "Sorry you can't make it D:"}
307 </div>
308 ),
309 type: "success",
310 });
311 } catch (error) {
312 smoker({
313 alignOnMobile: "left",
314 error: true,
315 text: "invalid code!",
316 position: {
317 x: rect ? rect.left + (rect.right - rect.left) / 2 : 0,
318 y: rect ? rect.top + 26 : 0,
319 },
320 });
321 return;
322 }
323 }}
324 >
325 <label className="rsvpNameInput relative w-full flex flex-col gap-0.5">
326 <div className="absolute top-0.5 left-[6px] text-xs font-bold italic text-tertiary">
327 confirmation code
328 </div>
329
330 <Input
331 autoFocus
332 placeholder="000000"
333 className="input-with-border pt-5! w-full "
334 value={props.value}
335 autoComplete="one-time-code"
336 onChange={(e) => props.onChange(e.target.value)}
337 />
338 <div className="text-sm italic text-tertiary leading-tight">
339 Code was sent to your{" "}
340 {props.country_code === "1" ? "phone" : <strong>WhatsApp</strong>}{" "}
341 number: +{props.country_code} {props.phoneNumber}!
342 </div>
343 </label>
344
345 <ButtonPrimary
346 id="rsvp-code-confirm-button"
347 className="place-self-end"
348 type="submit"
349 >
350 Confirm
351 </ButtonPrimary>
352 </form>
353 );
354};
355
356const ConsentPopover = (props: { country_code: string }) => {
357 return (
358 <Popover trigger={<InfoSmall className="text-accent-contrast" />}>
359 <div className="text-sm text-secondary">
360 By RSVPing I to consent to receive
361 {props.country_code === "1" ? "" : " WhatsApp"} messages from the event
362 host, via Leaflet!
363 </div>
364 </Popover>
365 );
366};