Something went wrong. Try again.
a tool for shared writing and social publishing
Something went wrong. Try again.
12 kB · 366 lines
TSX
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367"use client";import { useSmoker, useToaster } from "components/Toast";import { RSVP_Status, RSVPButtons, State, useRSVPNameState } from ".";import { createContext, useContext, useState } from "react";import { useRSVPData } from "components/PageSWRDataProvider";import { confirmPhoneAuthToken } from "actions/phone_auth/confirm_phone_auth_token";import { submitRSVP } from "actions/phone_rsvp_to_event";
import { countryCodes } from "src/constants/countryCodes";import { Checkbox } from "components/Checkbox";import { ButtonPrimary, ButtonTertiary } from "components/Buttons";import { Separator } from "components/Layout";import { createPhoneAuthToken } from "actions/phone_auth/request_phone_auth_token";import { Input, InputWithLabel } from "components/Input";import { RequestHeadersContext } from "components/Providers/RequestHeadersProvider";import { Popover } from "components/Popover";import { theme } from "tailwind.config";import { InfoSmall } from "components/Icons/InfoSmall";
export function ContactDetailsForm(props: { status: RSVP_Status; entityID: string; setState: (s: State) => void; setStatus: (s: RSVP_Status) => void;}) { let { status, entityID, setState, setStatus } = props; let focusWithinStyles = "focus-within:border-tertiary focus-within:outline-solid focus-within:outline-2 focus-within:outline-tertiary focus-within:outline-offset-1"; let toaster = useToaster(); let { data, mutate } = useRSVPData(); let [contactFormState, setContactFormState] = useState< { state: "details" } | { state: "confirm"; token: string } >({ state: "details" }); let { name, setName } = useRSVPNameState(); let [plus_ones, setPlusOnes] = useState( data?.rsvps?.find( (rsvp) => data.authToken && rsvp.entity === props.entityID && data.authToken.country_code === rsvp.country_code && data.authToken.phone_number === rsvp.phone_number, )?.plus_ones || 0, ); let requestHeaders = useContext(RequestHeadersContext); const [formState, setFormState] = useState({ country_code: countryCodes.find((c) => c[1].toUpperCase() === (requestHeaders.country || "US"))?.[2] || "1", phone_number: "", confirmationCode: "", });
let submit = async ( token: Awaited<ReturnType<typeof confirmPhoneAuthToken>>, ) => { try { await submitRSVP({ status, name: name, entity: entityID, plus_ones, }); } catch (e) { //handle failed confirm return false; }
mutate({ authToken: token, rsvps: [ ...(data?.rsvps || []).filter((r) => r.entity !== entityID), { name: name, status, plus_ones, entity: entityID, phone_number: token.phone_number, country_code: token.country_code, }, ], }); props.setState({ state: "default" }); return true; }; return contactFormState.state === "details" ? ( <> <form className="rsvpForm flex flex-col gap-2" onSubmit={async (e) => { e.preventDefault(); if (data?.authToken) { submit(data.authToken); toaster({ content: ( <div className="font-bold"> {status === "GOING" ? "Yay! You're Going!" : status === "MAYBE" ? "You're a Maybe" : "Sorry you can't make it D:"} </div> ), type: "success", }); } else { let tokenId = await createPhoneAuthToken(formState); setContactFormState({ state: "confirm", token: tokenId }); } }} > <RSVPButtons setStatus={props.setStatus} status={props.status} />
<div className="rsvpInputs flex sm:flex-row flex-col gap-2 w-fit place-self-center "> <label htmlFor="rsvp-name-input" className={` rsvpNameInput input-with-border h-fit flex flex-col ${focusWithinStyles}`} > <div className="text-xs font-bold italic text-tertiary">name</div> <Input autoFocus id="rsvp-name-input" placeholder="..." className=" bg-transparent disabled:text-tertiary w-full appearance-none focus:outline-0" value={name} onKeyDown={(e) => { if (e.key === "Backspace" && !e.currentTarget.value) e.preventDefault(); }} onChange={(e) => setName(e.target.value)} /> </label> <div className={`rsvpPhoneInputWrapper relative flex flex-col gap-0.5 w-full basis-2/3`} > <label htmlFor="rsvp-phone-input" className={` rsvpPhoneInput input-with-border flex flex-col ${focusWithinStyles} ${!!data?.authToken?.phone_number && "bg-border-light border-border-light text-tertiary"}`} > <div className=" text-xs font-bold italic text-tertiary"> Phone Number </div> <div className="flex gap-2 "> <div className="flex items-center gap-1"> <span style={{ color: formState.country_code === "" || !!data?.authToken?.phone_number ? theme.colors.tertiary : theme.colors.primary, }} > + </span> <Input onKeyDown={(e) => { if (e.key === "Backspace" && !e.currentTarget.value) e.preventDefault(); }} disabled={!!data?.authToken?.phone_number} className="w-10 bg-transparent appearance-none focus:outline-0" placeholder="1" maxLength={4} inputMode="numeric" pattern="[0-9]*" value={formState.country_code} onChange={(e) => setFormState((s) => ({ ...s, country_code: e.target.value.replace(/[^0-9]/g, ""), })) } /> </div> <Separator />
<Input id="rsvp-phone-input" inputMode="numeric" placeholder="0000000000" pattern="[0-9]*" className=" bg-transparent disabled:text-tertiary w-full appearance-none focus:outline-0" disabled={!!data?.authToken?.phone_number} onKeyDown={(e) => { if (e.key === "Backspace" && !e.currentTarget.value) e.preventDefault(); }} value={ data?.authToken?.phone_number || formState.phone_number } onChange={(e) => setFormState((state) => ({ ...state, phone_number: e.target.value.replace(/[^0-9]/g, ""), })) } /> </div> </label> <div className="text-xs italic text-tertiary leading-tight"> {formState.country_code !== "1" ? ( <> Messages to non-US/Canada numbers will be sent via{" "} <strong>WhatsApp</strong> </> ) : null} </div> </div> <div className="flex flex-row gap-2 w-full sm:w-32 h-fit"> <InputWithLabel className="appearance-none!" placeholder="0" label="Plus ones?" type="number" min={0} max={4} value={plus_ones} onChange={(e) => setPlusOnes(parseInt(e.currentTarget.value))} onKeyDown={(e) => { if (e.key === "Backspace" && !e.currentTarget.value) e.preventDefault(); }} /> </div> </div>
<hr className="border-border" /> <div className="flex flex-row gap-2 w-full items-center justify-end"> <ConsentPopover country_code={formState.country_code} /> <ButtonTertiary onMouseDown={() => { setState({ state: "default" }); }} > Back </ButtonTertiary> <ButtonPrimary disabled={ (!data?.authToken?.phone_number && (!formState.phone_number || !formState.country_code)) || !name } className="place-self-end" type="submit" > RSVP as{" "} {status === "GOING" ? "Going" : status === "MAYBE" ? "Maybe" : "Can't Go"} </ButtonPrimary> </div> </form> </> ) : ( <ConfirmationForm country_code={formState.country_code} phoneNumber={formState.phone_number} token={contactFormState.token} value={formState.confirmationCode} submit={submit} status={status} onChange={(value) => setFormState((state) => ({ ...state, confirmationCode: value })) } /> );}
const ConfirmationForm = (props: { country_code: string; phoneNumber: string; value: string; token: string; status: RSVP_Status; submit: ( token: Awaited<ReturnType<typeof confirmPhoneAuthToken>>, ) => Promise<boolean>; onChange: (v: string) => void;}) => { let smoker = useSmoker(); let toaster = useToaster(); return ( <form className="flex flex-col gap-3 w-full" onSubmit={async (e) => { e.preventDefault(); let rect = document .getElementById("rsvp-code-confirm-button") ?.getBoundingClientRect(); try { let token = await confirmPhoneAuthToken(props.token, props.value); props.submit(token); toaster({ content: ( <div className="font-bold"> {props.status === "GOING" ? "Yay! You're Going!" : props.status === "MAYBE" ? "You're a Maybe" : "Sorry you can't make it D:"} </div> ), type: "success", }); } catch (error) { smoker({ alignOnMobile: "left", error: true, text: "invalid code!", position: { x: rect ? rect.left + (rect.right - rect.left) / 2 : 0, y: rect ? rect.top + 26 : 0, }, }); return; } }} > <label className="rsvpNameInput relative w-full flex flex-col gap-0.5"> <div className="absolute top-0.5 left-[6px] text-xs font-bold italic text-tertiary"> confirmation code </div>
<Input autoFocus placeholder="000000" className="input-with-border pt-5! w-full " value={props.value} autoComplete="one-time-code" onChange={(e) => props.onChange(e.target.value)} /> <div className="text-sm italic text-tertiary leading-tight"> Code was sent to your{" "} {props.country_code === "1" ? "phone" : <strong>WhatsApp</strong>}{" "} number: +{props.country_code} {props.phoneNumber}! </div> </label>
<ButtonPrimary id="rsvp-code-confirm-button" className="place-self-end" type="submit" > Confirm </ButtonPrimary> </form> );};
const ConsentPopover = (props: { country_code: string }) => { return ( <Popover trigger={<InfoSmall className="text-accent-contrast" />}> <div className="text-sm text-secondary"> By RSVPing I to consent to receive {props.country_code === "1" ? "" : " WhatsApp"} messages from the event host, via Leaflet! </div> </Popover> );};