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