a tool for shared writing and social publishing
at feature/set-page-width 256 lines 7.6 kB view raw
1"use client"; 2import { Database } from "supabase/database.types"; 3import { BlockProps, BlockLayout } from "components/Blocks/Block"; 4import { useState } from "react"; 5import { submitRSVP } from "actions/phone_rsvp_to_event"; 6import { useRSVPData } from "components/PageSWRDataProvider"; 7import { useEntitySetContext } from "components/EntitySetProvider"; 8import { ButtonSecondary } from "components/Buttons"; 9import { create } from "zustand"; 10import { combine, createJSONStorage, persist } from "zustand/middleware"; 11import { useUIState } from "src/useUIState"; 12import { theme } from "tailwind.config"; 13import { useToaster } from "components/Toast"; 14import { ContactDetailsForm } from "./ContactDetailsForm"; 15import styles from "./RSVPBackground.module.css"; 16import { Attendees } from "./Atendees"; 17import { SendUpdateButton } from "./SendUpdate"; 18 19export type RSVP_Status = Database["public"]["Enums"]["rsvp_status"]; 20let Statuses = ["GOING", "NOT_GOING", "MAYBE"]; 21export type State = 22 | { 23 state: "default"; 24 } 25 | { state: "contact_details"; status: RSVP_Status }; 26 27export function RSVPBlock(props: BlockProps) { 28 let isSelected = useUIState((s) => 29 s.selectedBlocks.find((b) => b.value === props.entityID), 30 ); 31 return ( 32 <BlockLayout 33 isSelected={!!isSelected} 34 hasBackground={"accent"} 35 className="rsvp relative flex flex-col gap-1 w-full rounded-lg place-items-center justify-center" 36 > 37 <RSVPForm entityID={props.entityID} /> 38 </BlockLayout> 39 ); 40} 41 42function RSVPForm(props: { entityID: string }) { 43 let [state, setState] = useState<State>({ state: "default" }); 44 let { permissions } = useEntitySetContext(); 45 let { data, mutate } = useRSVPData(); 46 let setStatus = (status: RSVP_Status) => { 47 setState({ status, state: "contact_details" }); 48 }; 49 let [editing, setEditting] = useState(false); 50 51 let rsvpStatus = data?.rsvps?.find( 52 (rsvp) => 53 data.authToken && 54 rsvp.entity === props.entityID && 55 data.authToken.country_code === rsvp.country_code && 56 data.authToken.phone_number === rsvp.phone_number, 57 )?.status; 58 59 // IF YOU HAVE ALREADY RSVP'D 60 if (rsvpStatus && !editing) 61 return ( 62 <> 63 {permissions.write && <SendUpdateButton entityID={props.entityID} />} 64 65 <YourRSVPStatus 66 entityID={props.entityID} 67 setEditting={() => { 68 setEditting(true); 69 }} 70 /> 71 <div className="w-full flex justify-between"> 72 <Attendees entityID={props.entityID} /> 73 <button 74 className="hover:underline text-accent-contrast text-sm" 75 onClick={() => { 76 setStatus(rsvpStatus); 77 setEditting(true); 78 }} 79 > 80 Change RSVP 81 </button> 82 </div> 83 </> 84 ); 85 86 // IF YOU HAVEN'T RSVP'D 87 if (state.state === "default") 88 return ( 89 <> 90 {permissions.write && <SendUpdateButton entityID={props.entityID} />} 91 <RSVPButtons setStatus={setStatus} status={undefined} /> 92 <Attendees entityID={props.entityID} className="" /> 93 </> 94 ); 95 96 // IF YOU ARE CURRENTLY CONFIRMING YOUR CONTACT DETAILS 97 if (state.state === "contact_details") 98 return ( 99 <> 100 <ContactDetailsForm 101 status={state.status} 102 setStatus={setStatus} 103 setState={(newState) => { 104 if (newState.state === "default" && editing) setEditting(false); 105 setState(newState); 106 }} 107 entityID={props.entityID} 108 /> 109 </> 110 ); 111} 112 113export const RSVPButtons = (props: { 114 setStatus: (status: RSVP_Status) => void; 115 status: RSVP_Status | undefined; 116}) => { 117 return ( 118 <div className="relative w-full sm:p-6 py-4 px-3 rounded-md border-[1.5px] border-accent-1"> 119 <RSVPBackground /> 120 <div className="relative flex flex-row gap-2 items-center mx-auto z-1 w-fit"> 121 <ButtonSecondary 122 type="button" 123 className={ 124 props.status === "MAYBE" 125 ? "text-accent-2! bg-accent-1! text-lg" 126 : "" 127 } 128 onClick={() => props.setStatus("MAYBE")} 129 > 130 Maybe 131 </ButtonSecondary> 132 <ButtonSecondary 133 type="button" 134 className={ 135 props.status === "GOING" 136 ? "text-accent-2! bg-accent-1! text-lg" 137 : props.status === undefined 138 ? "text-lg" 139 : "" 140 } 141 onClick={() => props.setStatus("GOING")} 142 > 143 Going! 144 </ButtonSecondary> 145 146 <ButtonSecondary 147 type="button" 148 className={ 149 props.status === "NOT_GOING" 150 ? "text-accent-2! bg-accent-1! text-lg" 151 : "" 152 } 153 onClick={() => props.setStatus("NOT_GOING")} 154 > 155 Can&apos;t Go 156 </ButtonSecondary> 157 </div> 158 </div> 159 ); 160}; 161 162function YourRSVPStatus(props: { 163 entityID: string; 164 compact?: boolean; 165 setEditting: (e: boolean) => void; 166}) { 167 let { data, mutate } = useRSVPData(); 168 let { name } = useRSVPNameState(); 169 let toaster = useToaster(); 170 171 let existingRSVP = data?.rsvps?.find( 172 (rsvp) => 173 data.authToken && 174 rsvp.entity === props.entityID && 175 data.authToken.phone_number === rsvp.phone_number, 176 ); 177 let rsvpStatus = existingRSVP?.status; 178 179 let updateStatus = async (status: RSVP_Status) => { 180 if (!data?.authToken) return; 181 await submitRSVP({ 182 status, 183 name: name, 184 entity: props.entityID, 185 plus_ones: existingRSVP?.plus_ones || 0, 186 }); 187 188 mutate({ 189 authToken: data.authToken, 190 rsvps: [ 191 ...(data?.rsvps || []).filter((r) => r.entity !== props.entityID), 192 { 193 name: name, 194 status, 195 entity: props.entityID, 196 phone_number: data.authToken.phone_number, 197 country_code: data.authToken.country_code, 198 plus_ones: existingRSVP?.plus_ones || 0, 199 }, 200 ], 201 }); 202 }; 203 return ( 204 <div 205 className={`relative w-full p-4 pb-5 rounded-md border-[1.5px] border-accent-1 font-bold items-center`} 206 > 207 <RSVPBackground /> 208 <div className=" relative flex flex-col gap-1 sm:gap-2 z-1 justify-center w-fit mx-auto"> 209 <div 210 className=" w-fit text-xl text-center text-accent-2" 211 style={{ 212 WebkitTextStroke: `3px ${theme.colors["accent-1"]}`, 213 textShadow: `-4px 3px 0 ${theme.colors["accent-1"]}`, 214 paintOrder: "stroke fill", 215 }} 216 > 217 {rsvpStatus !== undefined && 218 { 219 GOING: `You're Going!`, 220 MAYBE: "You're a Maybe", 221 NOT_GOING: "Can't Make It", 222 }[rsvpStatus]} 223 </div> 224 {existingRSVP?.plus_ones && existingRSVP?.plus_ones > 0 ? ( 225 <div className="absolute -top-2 -right-6 rotate-12 h-fit w-10 bg-accent-1 font-bold text-accent-2 rounded-full -z-10"> 226 <div className="w-full text-center pr-[4px] pb-px"> 227 +{existingRSVP?.plus_ones} 228 </div> 229 </div> 230 ) : null} 231 </div> 232 </div> 233 ); 234} 235 236const RSVPBackground = () => { 237 return ( 238 <div className="overflow-hidden absolute top-0 bottom-0 left-0 right-0 "> 239 <div 240 className={`rsvp-background w-full h-full bg-accent-1 z-0 ${styles.RSVPWavyBG} `} 241 /> 242 </div> 243 ); 244}; 245 246export let useRSVPNameState = create( 247 persist( 248 combine({ name: "" }, (set) => ({ 249 setName: (name: string) => set({ name }), 250 })), 251 { 252 name: "rsvp-name", 253 storage: createJSONStorage(() => localStorage), 254 }, 255 ), 256);