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