a tool for shared writing and social publishing

Feature/whatsapp rsvps (#111)

* enable rsvp blocks

* fix up the consent studd and the help text

* disable the rsvp button if no name is provided

* used form elements to get submission on enter keypress, adjusted copy in the send an update form to be shorter

* added ability to send messages based on rsvp status

* some spaceing tweaks, added a counter to the send button

* added toasts when rsvp is successful

* the vibes update yo

* added an are you sure state to delete

* some tweaks to the wavy bg

* fix swr key conflict and always fetch rsvp_data

* tweak consent copy

* use new template and add link to leaflet

* update rsvp text blast whatsapp template

---------

Co-authored-by: celine <celine@hyperlink.academy>

authored by awarm.space

celine and committed by
GitHub
c3c85a6a 03aaabc9

+442 -215
+15 -7
actions/getRSVPData.ts
··· 12 12 13 13 export async function getRSVPData(entity_sets: string[]) { 14 14 const token = cookies().get("phone_auth_token"); 15 - if (!token) { 16 - return null; 17 - } 18 15 19 16 const client = postgres(process.env.DB_URL as string, { idle_timeout: 5 }); 20 17 const db = drizzle(client); 21 18 22 - const [authToken] = await db 23 - .select() 24 - .from(phone_number_auth_tokens) 25 - .where(eq(phone_number_auth_tokens.id, token.value)); 19 + let authToken: { 20 + id: string; 21 + created_at: string; 22 + confirmed: boolean; 23 + confirmation_code: string; 24 + phone_number: string; 25 + country_code: string; 26 + } | null = null; 27 + if (token) { 28 + const data = await db 29 + .select() 30 + .from(phone_number_auth_tokens) 31 + .where(eq(phone_number_auth_tokens.id, token.value)); 32 + authToken = data[0]; 33 + } 26 34 27 35 const rsvps = await db 28 36 .select()
+21 -10
actions/sendUpdateToRSVPS.ts
··· 10 10 import { createClient } from "@supabase/supabase-js"; 11 11 import { Database } from "supabase/database.types"; 12 12 import twilio from "twilio"; 13 - import { Message } from "twilio/lib/twiml/MessagingResponse"; 14 13 15 14 const client = postgres(process.env.DB_URL as string, { idle_timeout: 5 }); 16 15 let supabase = createClient<Database>( ··· 25 24 entity, 26 25 message, 27 26 eventName, 28 - }: { entity: string; message: string; eventName: string }, 27 + sendto, 28 + publicLeafletID, 29 + }: { 30 + entity: string; 31 + message: string; 32 + eventName: string; 33 + publicLeafletID: string; 34 + sendto: { GOING: boolean; MAYBE: boolean; NOT_GOING: boolean }; 35 + }, 29 36 ) { 30 37 let token_rights = await db 31 38 .select() ··· 50 57 const client = twilio(accountSid, authToken); 51 58 52 59 for (let rsvp of rsvps) { 53 - const result = await client.messages.create({ 54 - contentSid: "HX2755dab400476ade5effd90e2d964e6c", 55 - contentVariables: JSON.stringify({ 1: eventName, 2: message }), 56 - from: "whatsapp:+18449523391", 57 - messagingServiceSid: "MGffbf9a66770350b25caf3b80b9aac481", 58 - to: `whatsapp:${rsvp.phone_rsvps_to_entity.phone_number}`, 59 - }); 60 - console.log(result); 60 + if (sendto[rsvp.phone_rsvps_to_entity.status]) 61 + await client.messages.create({ 62 + contentSid: "HX8e1217f791d38fa4cf7b7b24a02fe10c", 63 + contentVariables: JSON.stringify({ 64 + 1: eventName, 65 + 2: message, 66 + 3: `https://leaflet.pub/${publicLeafletID}`, 67 + }), 68 + from: "whatsapp:+18449523391", 69 + messagingServiceSid: "MGffbf9a66770350b25caf3b80b9aac481", 70 + to: `whatsapp:${rsvp.phone_rsvps_to_entity.phone_number}`, 71 + }); 61 72 } 62 73 }
+2 -2
app/[leaflet_id]/page.tsx
··· 50 50 </div> 51 51 ); 52 52 53 - let [{ data }, identity_data] = await Promise.all([ 53 + let [{ data }, rsvp_data] = await Promise.all([ 54 54 supabase.rpc("get_facts", { 55 55 root: rootEntity, 56 56 }), ··· 58 58 ]); 59 59 let initialFacts = (data as unknown as Fact<keyof typeof Attributes>[]) || []; 60 60 return ( 61 - <RSVPDataProvider data={identity_data}> 61 + <RSVPDataProvider data={rsvp_data}> 62 62 <Leaflet 63 63 initialFacts={initialFacts} 64 64 leaflet_id={rootEntity}
+17
app/globals.css
··· 13 13 --accent-1: 0, 0, 225; 14 14 --accent-2: 255, 255, 255; 15 15 --accent-contrast: 0, 0, 225; 16 + --accent-1-is-contrast: "true"; 16 17 17 18 --highlight-1: 255, 177, 177; 18 19 --highlight-2: 253, 245, 203; ··· 25 26 --gripperSVG: url("/gripperPattern.svg"); 26 27 --gripperSVG2: url("/gripperPattern2.svg"); 27 28 --hatchSVG: url("/hatchPattern.svg"); 29 + --wavySVG: (url("/RSVPBackground/wavy.svg")); 28 30 } 29 31 30 32 @media (max-width: 640px) { ··· 223 225 @apply outline; 224 226 @apply outline-transparent; 225 227 } 228 + 229 + .text-with-outline { 230 + position: relative; 231 + -webkit-text-stroke: 1px purple; 232 + z-index: 1; 233 + 234 + ::before { 235 + content: attr(data-text); 236 + position: absolute; 237 + top: 0; 238 + left: 0; 239 + color: blue; 240 + z-index: 0; 241 + } 242 + }
+2 -8
components/Blocks/Block.tsx
··· 15 15 import { EmbedBlock } from "./EmbedBlock"; 16 16 import { MailboxBlock } from "./MailboxBlock"; 17 17 import { HeadingBlock } from "./HeadingBlock"; 18 - import { 19 - CheckboxChecked, 20 - CheckboxEmpty, 21 - LockTiny, 22 - UnlockSmall, 23 - UnlockTiny, 24 - } from "components/Icons"; 18 + import { CheckboxChecked, CheckboxEmpty, LockTiny } from "components/Icons"; 25 19 import { AreYouSure } from "./DeleteBlock"; 26 20 import { useEntitySetContext } from "components/EntitySetProvider"; 27 - import { Media } from "components/Media"; 28 21 import { useIsMobile } from "src/hooks/isMobile"; 29 22 import { DateTimeBlock } from "./DateTimeBlock"; 30 23 import { RSVPBlock } from "./RSVPBlock"; ··· 84 77 } 85 78 }, [selected]); 86 79 80 + // THIS IS WHERE YOU SET WHETHER OR NOT AREYOUSURE IS TRIGGERED ON THE DELETE KEY 87 81 useBlockKeyboardHandlers(props, areYouSure, setAreYouSure); 88 82 89 83 return (
+6 -6
components/Blocks/BlockCommands.tsx
··· 198 198 199 199 // EVENT STUFF 200 200 201 - // { 202 - // name: "RSVP", 203 - // icon: <RSVPSmall />, 204 - // type: "event", 205 - // onSelect: (rep, props) => createBlockWithType(rep, props, "rsvp"), 206 - // }, 201 + { 202 + name: "RSVP", 203 + icon: <RSVPSmall />, 204 + type: "event", 205 + onSelect: (rep, props) => createBlockWithType(rep, props, "rsvp"), 206 + }, 207 207 { 208 208 name: "Date and Time", 209 209 icon: <BlockCalendarSmall />,
+113 -61
components/Blocks/RSVPBlock/ContactDetailsForm.tsx
··· 1 1 "use client"; 2 - import { useSmoker } from "components/Toast"; 2 + import { useSmoker, useToaster } from "components/Toast"; 3 3 import { RSVP_Status, State, useRSVPNameState } from "."; 4 4 import { createContext, useContext, useState } from "react"; 5 5 import { useRSVPData } from "src/hooks/useRSVPData"; ··· 8 8 9 9 import { countryCodes } from "src/constants/countryCodes"; 10 10 import { Checkbox } from "components/Checkbox"; 11 - import { ButtonPrimary } from "components/Buttons"; 11 + import { ButtonPrimary, ButtonTertiary } from "components/Buttons"; 12 12 import { Separator } from "components/Layout"; 13 13 import { createPhoneAuthToken } from "actions/phone_auth/request_phone_auth_token"; 14 14 import { Input } from "components/Input"; 15 15 import { IPLocationContext } from "components/Providers/IPLocationProvider"; 16 + import { Popover } from "components/Popover"; 17 + import { InfoSmall } from "components/Icons"; 18 + import { theme } from "tailwind.config"; 16 19 17 - export function ContactDetailsForm({ 18 - status, 19 - entityID, 20 - }: { 20 + export function ContactDetailsForm(props: { 21 21 status: RSVP_Status; 22 22 entityID: string; 23 23 setState: (s: State) => void; 24 24 }) { 25 + let { status, entityID, setState } = props; 25 26 let focusWithinStyles = 26 27 "focus-within:border-tertiary focus-within:outline focus-within:outline-2 focus-within:outline-tertiary focus-within:outline-offset-1"; 27 - let [checked, setChecked] = useState(false); 28 - 28 + let toaster = useToaster(); 29 29 let { data, mutate } = useRSVPData(); 30 - let [state, setState] = useState< 30 + let [contactFormState, setContactFormState] = useState< 31 31 { state: "details" } | { state: "confirm"; token: string } 32 32 >({ state: "details" }); 33 33 let { name, setName } = useRSVPNameState(); ··· 38 38 phone_number: "", 39 39 confirmationCode: "", 40 40 }); 41 - let [enterNewNumber, setEnterNewNumber] = useState(false); 42 41 43 42 let submit = async ( 44 43 token: Awaited<ReturnType<typeof confirmPhoneAuthToken>>, ··· 69 68 }); 70 69 return true; 71 70 }; 72 - return state.state === "details" ? ( 73 - <div className="rsvpForm flex flex-col gap-2"> 71 + return contactFormState.state === "details" ? ( 72 + <form 73 + className="rsvpForm flex flex-col gap-2" 74 + onSubmit={async (e) => { 75 + e.preventDefault(); 76 + if (data?.authToken) { 77 + submit(data.authToken); 78 + toaster({ 79 + content: ( 80 + <div className="font-bold"> 81 + {status === "GOING" 82 + ? "Yay! You're Going!" 83 + : status === "MAYBE" 84 + ? "You're a Maybe" 85 + : "Sorry you can't make it D:"} 86 + </div> 87 + ), 88 + type: "success", 89 + }); 90 + } else { 91 + let tokenId = await createPhoneAuthToken(formState); 92 + setContactFormState({ state: "confirm", token: tokenId }); 93 + } 94 + }} 95 + > 74 96 <div className="rsvpInputs flex sm:flex-row flex-col gap-2 w-fit place-self-center "> 75 97 <label 76 98 htmlFor="rsvp-name-input" ··· 103 125 </div> 104 126 <div className="flex gap-2 "> 105 127 <div className="flex items-center gap-1"> 106 - <span>+</span> 128 + <span 129 + style={{ 130 + color: 131 + formState.country_code === "" 132 + ? theme.colors.tertiary 133 + : theme.colors.primary, 134 + }} 135 + > 136 + + 137 + </span> 107 138 <Input 108 139 onKeyDown={(e) => { 109 140 if (e.key === "Backspace" && !e.currentTarget.value) 110 141 e.preventDefault(); 111 142 }} 112 143 disabled={!!data?.authToken?.phone_number} 113 - className="w-10 bg-transparent" 144 + className="w-10 bg-transparent appearance-none focus:outline-0" 114 145 placeholder="1" 115 146 maxLength={4} 116 147 inputMode="numeric" ··· 148 179 </div> 149 180 </label> 150 181 <div className="text-xs italic text-tertiary leading-tight"> 151 - Non-US numbers will receive messages through{" "} 152 - <strong>WhatsApp</strong> 182 + Currently, all communication will be routed through{" "} 183 + <strong>WhatsApp</strong>. SMS coming soon! 153 184 </div> 154 185 </div> 155 186 </div> 156 187 157 188 <hr className="border-border" /> 158 189 <div className="flex flex-row gap-2 w-full items-center justify-end"> 159 - <ConsentPopover checked={checked} setChecked={setChecked} /> 190 + <ConsentPopover /> 191 + <ButtonTertiary 192 + onMouseDown={() => { 193 + setState({ state: "default" }); 194 + }} 195 + > 196 + Back 197 + </ButtonTertiary> 160 198 <ButtonPrimary 161 199 disabled={ 162 200 (!data?.authToken?.phone_number && 163 - (!checked || 164 - !formState.phone_number || 165 - !formState.country_code)) || 166 - (!!data?.authToken?.phone_number && !checked) 201 + (!formState.phone_number || !formState.country_code)) || 202 + !name 167 203 } 168 204 className="place-self-end" 169 - onClick={async () => { 170 - if (data?.authToken) { 171 - submit(data.authToken); 172 - } else { 173 - let tokenId = await createPhoneAuthToken(formState); 174 - setState({ state: "confirm", token: tokenId }); 175 - } 176 - }} 205 + type="submit" 177 206 > 178 - RSVP as {status === "GOING" ? "Going" : "Maybe"} 207 + RSVP as{" "} 208 + {status === "GOING" 209 + ? "Going" 210 + : status === "MAYBE" 211 + ? "Maybe" 212 + : "Can't Go"} 179 213 </ButtonPrimary> 180 214 </div> 181 - </div> 215 + </form> 182 216 ) : ( 183 217 <ConfirmationForm 184 - token={state.token} 218 + token={contactFormState.token} 185 219 value={formState.confirmationCode} 186 220 submit={submit} 221 + status={status} 187 222 onChange={(value) => 188 223 setFormState((state) => ({ ...state, confirmationCode: value })) 189 224 } ··· 194 229 const ConfirmationForm = (props: { 195 230 value: string; 196 231 token: string; 232 + status: RSVP_Status; 197 233 submit: ( 198 234 token: Awaited<ReturnType<typeof confirmPhoneAuthToken>>, 199 235 ) => Promise<boolean>; 200 236 onChange: (v: string) => void; 201 237 }) => { 202 238 let smoker = useSmoker(); 239 + let toaster = useToaster(); 203 240 return ( 204 - <div className="flex flex-col gap-2"> 241 + <form 242 + className="flex flex-col gap-2" 243 + onSubmit={async (e) => { 244 + e.preventDefault(); 245 + let rect = document 246 + .getElementById("rsvp-code-confirm-button") 247 + ?.getBoundingClientRect(); 248 + try { 249 + let token = await confirmPhoneAuthToken(props.token, props.value); 250 + props.submit(token); 251 + toaster({ 252 + content: ( 253 + <div className="font-bold"> 254 + {props.status === "GOING" 255 + ? "Yay! You're Going!" 256 + : props.status === "MAYBE" 257 + ? "You're a Maybe" 258 + : "Sorry you can't make it D:"} 259 + </div> 260 + ), 261 + type: "success", 262 + }); 263 + } catch (error) { 264 + smoker({ 265 + alignOnMobile: "left", 266 + error: true, 267 + text: "invalid code!", 268 + position: { 269 + x: rect ? rect.left + (rect.right - rect.left) / 2 : 0, 270 + y: rect ? rect.top + 26 : 0, 271 + }, 272 + }); 273 + return; 274 + } 275 + }} 276 + > 205 277 <label className="rsvpNameInput relative w-full flex flex-col gap-0.5"> 206 278 <div className="absolute top-0.5 left-[6px] text-xs font-bold italic text-tertiary"> 207 279 confirmation code ··· 220 292 <hr className="border-border" /> 221 293 222 294 <ButtonPrimary 295 + id="rsvp-code-confirm-button" 223 296 className="place-self-end" 224 - onMouseDown={async (e) => { 225 - try { 226 - let token = await confirmPhoneAuthToken(props.token, props.value); 227 - props.submit(token); 228 - } catch (error) { 229 - smoker({ 230 - alignOnMobile: "left", 231 - error: true, 232 - text: "invalid code!", 233 - position: { x: e.clientX, y: e.clientY }, 234 - }); 235 - return; 236 - } 237 - }} 297 + type="submit" 238 298 > 239 299 Confirm 240 300 </ButtonPrimary> 241 - </div> 301 + </form> 242 302 ); 243 303 }; 244 304 245 - const ConsentPopover = (props: { 246 - checked: boolean; 247 - setChecked: (checked: boolean) => void; 248 - }) => { 305 + const ConsentPopover = (props: {}) => { 249 306 return ( 250 - <Checkbox 251 - checked={props.checked} 252 - onChange={() => { 253 - props.setChecked(!props.checked); 254 - }} 255 - > 307 + <Popover trigger={<InfoSmall className="text-accent-contrast" />}> 256 308 <div className="text-sm text-secondary"> 257 - Clicking RSVP means that you are consenting to receive WhatsApp messages 258 - from the host of this event, via Leaflet! 309 + By RSVPing I to consent to receive WhatsApp messages from the event 310 + host, via Leaflet! 259 311 </div> 260 - </Checkbox> 312 + </Popover> 261 313 ); 262 314 };
+7
components/Blocks/RSVPBlock/RSVPBackground.module.css
··· 1 + .RSVPWavyBG { 2 + opacity: calc(1 - (var(--accent-1-is-contrast) * 0.6)); 3 + mask-image: url("/RSVPBackground/wavy.svg"); 4 + mask-repeat: repeat repeat; 5 + mask-position: center; 6 + mask-size: 64px; 7 + }
+215 -109
components/Blocks/RSVPBlock/index.tsx
··· 15 15 import { create } from "zustand"; 16 16 import { combine, createJSONStorage, persist } from "zustand/middleware"; 17 17 import { useUIState } from "src/useUIState"; 18 - import { Separator } from "components/Layout"; 19 18 import { theme } from "tailwind.config"; 20 19 import { useToaster } from "components/Toast"; 21 20 import { sendUpdateToRSVPS } from "actions/sendUpdateToRSVPS"; 22 21 import { useReplicache } from "src/replicache"; 23 22 import { ContactDetailsForm } from "./ContactDetailsForm"; 23 + import { Checkbox } from "components/Checkbox"; 24 + import styles from "./RSVPBackground.module.css"; 25 + import { usePublishLink } from "components/ShareOptions"; 24 26 25 27 export type RSVP_Status = Database["public"]["Enums"]["rsvp_status"]; 26 28 let Statuses = ["GOING", "NOT_GOING", "MAYBE"]; ··· 36 38 ); 37 39 return ( 38 40 <div 39 - className={`rsvp flex flex-col sm:gap-2 border bg-test p-3 w-full rounded-lg ${isSelected ? "block-border-selected " : "block-border"}`} 41 + 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"}`} 40 42 style={{ 41 43 backgroundColor: 42 - "color-mix(in oklab, rgb(var(--accent-contrast)), rgb(var(--bg-page)) 85%)", 44 + "color-mix(in oklab, rgb(var(--accent-1)), rgb(var(--bg-page)) 85%)", 43 45 }} 44 46 > 45 47 <RSVPForm entityID={props.entityID} /> ··· 47 49 ); 48 50 } 49 51 52 + const RSVPBackground = () => { 53 + return ( 54 + <div className="overflow-hidden absolute top-0 bottom-0 left-0 right-0 "> 55 + <div 56 + className={`rsvp-background w-full h-full bg-accent-1 z-0 ${styles.RSVPWavyBG} `} 57 + /> 58 + </div> 59 + ); 60 + }; 61 + 50 62 function RSVPForm(props: { entityID: string }) { 51 63 let [state, setState] = useState<State>({ state: "default" }); 52 64 let { permissions } = useEntitySetContext(); ··· 65 77 66 78 // IF YOU HAVE ALREADY RSVP'D 67 79 if (rsvpStatus) 68 - return permissions.write ? ( 69 - //AND YOU'RE A HOST 80 + return ( 70 81 <> 71 - <div className="flex sm:flex-row flex-col-reverse sm:gap-0 gap-2 justify-between items-start sm:items-center"> 72 - <Attendees entityID={props.entityID} className="font-bold" /> 73 - <hr className="block border-border sm:hidden w-full my-1" /> 82 + {permissions.write && <SendUpdateButton entityID={props.entityID} />} 74 83 75 - <SendUpdateButton entityID={props.entityID} /> 76 - </div> 77 - <hr className="border-border w-full hidden sm:block" /> 78 - <YourRSVPStatus entityID={props.entityID} compact /> 79 - </> 80 - ) : ( 81 - // AND YOU'RE A GUEST 82 - <div className="flex sm:flex-row flex-col justify-between items-start sm:items-center"> 83 84 <YourRSVPStatus entityID={props.entityID} /> 84 - <hr className="block border-border sm:hidden w-full my-2" /> 85 - <Attendees entityID={props.entityID} className="font-normal text-sm" /> 86 - </div> 85 + <Attendees entityID={props.entityID} /> 86 + </> 87 87 ); 88 88 89 89 // IF YOU HAVEN'T RSVP'D 90 90 if (state.state === "default") 91 - return permissions.write ? ( 92 - //YOU'RE A HOST 91 + return ( 93 92 <> 94 - <div className="flex sm:flex-row flex-col-reverse sm:gap-0 gap-2 justify-between"> 95 - <div className="flex flex-row gap-2 items-center"> 96 - <ButtonPrimary onClick={() => setStatus("GOING")}> 97 - Going! 98 - </ButtonPrimary> 99 - <ButtonSecondary onClick={() => setStatus("MAYBE")}> 100 - Maybe 101 - </ButtonSecondary> 102 - <ButtonTertiary onClick={() => setStatus("NOT_GOING")}> 103 - Can&apos;t Go 104 - </ButtonTertiary> 105 - </div> 106 - <hr className="block border-border sm:hidden w-full my-1" /> 107 - 108 - <SendUpdateButton entityID={props.entityID} /> 109 - </div> 110 - <hr className="border-border sm:block hidden" /> 111 - <Attendees entityID={props.entityID} className="text-sm sm:pt-0 pt-2" /> 93 + {permissions.write && <SendUpdateButton entityID={props.entityID} />} 94 + <RSVPButtons setStatus={setStatus} /> 95 + <Attendees entityID={props.entityID} className="" /> 112 96 </> 113 - ) : ( 114 - //YOU'RE A GUEST 115 - <div className="flex sm:flex-row flex-col justify-between"> 116 - <div className="flex flex-row gap-2 items-center"> 117 - <ButtonPrimary onClick={() => setStatus("GOING")}> 118 - Going! 119 - </ButtonPrimary> 120 - <ButtonSecondary onClick={() => setStatus("MAYBE")}> 121 - Maybe 122 - </ButtonSecondary> 123 - <ButtonTertiary onClick={() => setStatus("NOT_GOING")}> 124 - Can&apos;t Go 125 - </ButtonTertiary> 126 - </div> 127 - <hr className="block border-border sm:hidden w-full my-2" /> 128 - 129 - <Attendees entityID={props.entityID} className="text-sm" /> 130 - </div> 131 97 ); 132 98 133 99 // IF YOU ARE CURRENTLY CONFIRMING YOUR CONTACT DETAILS ··· 141 107 ); 142 108 } 143 109 110 + const RSVPButtons = (props: { setStatus: (status: RSVP_Status) => void }) => { 111 + return ( 112 + <div className="relative w-full sm:p-6 py-4 px-3 rounded-md border-[1.5px] border-accent-1"> 113 + <RSVPBackground /> 114 + <div className="relative flex flex-row gap-2 items-center place-self-center z-[1]"> 115 + <ButtonSecondary className="" onClick={() => props.setStatus("MAYBE")}> 116 + Maybe 117 + </ButtonSecondary> 118 + <ButtonPrimary 119 + className="text-lg" 120 + onClick={() => props.setStatus("GOING")} 121 + > 122 + Going! 123 + </ButtonPrimary> 124 + 125 + <ButtonSecondary 126 + className="" 127 + onClick={() => props.setStatus("NOT_GOING")} 128 + > 129 + Can&apos;t Go 130 + </ButtonSecondary> 131 + </div> 132 + </div> 133 + ); 134 + }; 135 + 144 136 function YourRSVPStatus(props: { entityID: string; compact?: boolean }) { 145 137 let { data, mutate } = useRSVPData(); 146 138 let { name } = useRSVPNameState(); 139 + let toaster = useToaster(); 147 140 148 141 let rsvpStatus = data?.rsvps?.find( 149 142 (rsvp) => ··· 176 169 }; 177 170 return ( 178 171 <div 179 - className={`flex flex-row gap-1 sm:gap-2 font-bold items-center ${props.compact ? "text-sm sm:font-bold font-normal" : ""}`} 172 + className={`relative w-full p-4 pb-5 rounded-md border-[1.5px] border-accent-1 font-bold items-center`} 180 173 > 181 - {rsvpStatus !== undefined && 182 - { 183 - GOING: `You're Going!`, 184 - MAYBE: "You're a Maybe", 185 - NOT_GOING: "Can't Make It", 186 - }[rsvpStatus]} 187 - <Separator classname="mx-1 h-6" /> 188 - {rsvpStatus !== "GOING" && ( 189 - <ButtonPrimary 190 - className={props.compact ? "text-sm !font-normal" : ""} 191 - compact={props.compact} 192 - onClick={() => updateStatus("GOING")} 174 + <RSVPBackground /> 175 + <div className=" relative flex flex-col gap-1 sm:gap-2 z-[1] justify-center"> 176 + <div 177 + className="text-xl text-center text-accent-2 text-with-outline" 178 + style={{ 179 + WebkitTextStroke: `3px ${theme.colors["accent-1"]}`, 180 + textShadow: `-4px 3px 0 ${theme.colors["accent-1"]}`, 181 + paintOrder: "stroke fill", 182 + }} 193 183 > 194 - Going 195 - </ButtonPrimary> 196 - )} 197 - {rsvpStatus !== "MAYBE" && ( 198 - <ButtonSecondary 199 - className={props.compact ? "text-sm !font-normal" : ""} 200 - compact={props.compact} 201 - onClick={() => updateStatus("MAYBE")} 202 - > 203 - Maybe 204 - </ButtonSecondary> 205 - )} 206 - {rsvpStatus !== "NOT_GOING" && ( 207 - <ButtonTertiary 208 - className={props.compact ? "text-sm !font-normal" : ""} 209 - onClick={() => updateStatus("NOT_GOING")} 210 - > 211 - Can&apos;t Go 212 - </ButtonTertiary> 213 - )} 184 + {rsvpStatus !== undefined && 185 + { 186 + GOING: `You're Going!`, 187 + MAYBE: "You're a Maybe", 188 + NOT_GOING: "Can't Make It", 189 + }[rsvpStatus]} 190 + </div> 191 + <div className="flex gap-4 place-items-center justify-center"> 192 + {rsvpStatus !== "GOING" && ( 193 + <ButtonSecondary 194 + className={props.compact ? "text-sm !font-normal" : ""} 195 + compact 196 + onClick={() => { 197 + updateStatus("GOING"); 198 + toaster({ 199 + content: ( 200 + <div className="font-bold">Yay! You&apos;re Going!</div> 201 + ), 202 + type: "success", 203 + }); 204 + }} 205 + > 206 + Going 207 + </ButtonSecondary> 208 + )} 209 + {rsvpStatus !== "MAYBE" && ( 210 + <ButtonSecondary 211 + className={props.compact ? "text-sm !font-normal" : ""} 212 + compact 213 + onClick={() => { 214 + updateStatus("MAYBE"); 215 + toaster({ 216 + content: <div className="font-bold">You&apos;re a Maybe</div>, 217 + type: "success", 218 + }); 219 + }} 220 + > 221 + Maybe 222 + </ButtonSecondary> 223 + )} 224 + {rsvpStatus !== "NOT_GOING" && ( 225 + <ButtonSecondary 226 + compact 227 + className={props.compact ? "text-sm !font-normal" : ""} 228 + onClick={() => { 229 + updateStatus("NOT_GOING"); 230 + toaster({ 231 + content: ( 232 + <div className="font-bold"> 233 + Sorry you can&apos;t make it D: 234 + </div> 235 + ), 236 + type: "success", 237 + }); 238 + }} 239 + > 240 + Can&apos;t Go 241 + </ButtonSecondary> 242 + )} 243 + </div> 244 + </div> 214 245 </div> 215 246 ); 216 247 } 217 248 218 249 function Attendees(props: { entityID: string; className?: string }) { 219 - let { data, mutate } = useRSVPData(); 250 + let { data } = useRSVPData(); 220 251 let attendees = 221 - data?.rsvps.filter((rsvp) => rsvp.entity === props.entityID) || []; 252 + data?.rsvps?.filter((rsvp) => rsvp.entity === props.entityID) || []; 222 253 let going = attendees.filter((rsvp) => rsvp.status === "GOING"); 223 254 let maybe = attendees.filter((rsvp) => rsvp.status === "MAYBE"); 224 255 let notGoing = attendees.filter((rsvp) => rsvp.status === "NOT_GOING"); ··· 231 262 trigger={ 232 263 going.length === 0 && maybe.length === 0 ? ( 233 264 <button 234 - className={`w-max text-tertiary italic hover:underline ${props.className}`} 265 + className={`text-sm font-normal w-max text-tertiary italic hover:underline ${props.className}`} 235 266 > 236 267 No RSVPs yet 237 268 </button> 238 269 ) : ( 239 - <ButtonTertiary className={props.className}> 270 + <ButtonTertiary className={`text-sm font-normal ${props.className}`}> 240 271 {going.length > 0 && `${going.length} Going`} 241 272 {maybe.length > 0 && 242 273 `${going.length > 0 ? ", " : ""}${maybe.length} Maybe`} ··· 278 309 } 279 310 280 311 function SendUpdateButton(props: { entityID: string }) { 312 + let publishLink = usePublishLink(); 281 313 let { permissions } = useEntitySetContext(); 282 314 let { permission_token } = useReplicache(); 283 315 let [input, setInput] = useState(""); 284 316 let toaster = useToaster(); 285 317 let [open, setOpen] = useState(false); 318 + let [checkedRecipients, setCheckedRecipients] = useState({ 319 + GOING: true, 320 + MAYBE: true, 321 + NOT_GOING: false, 322 + }); 323 + 324 + let { data, mutate } = useRSVPData(); 325 + let attendees = 326 + data?.rsvps?.filter((rsvp) => rsvp.entity === props.entityID) || []; 327 + let going = attendees.filter((rsvp) => rsvp.status === "GOING"); 328 + let maybe = attendees.filter((rsvp) => rsvp.status === "MAYBE"); 329 + let notGoing = attendees.filter((rsvp) => rsvp.status === "NOT_GOING"); 330 + 331 + let allRecipients = 332 + ((checkedRecipients.GOING && going.length) || 0) + 333 + ((checkedRecipients.MAYBE && maybe.length) || 0) + 334 + ((checkedRecipients.NOT_GOING && notGoing.length) || 0); 286 335 287 336 if (!!!permissions.write) return; 288 337 return ( ··· 291 340 open={open} 292 341 onOpenChange={(open) => setOpen(open)} 293 342 trigger={ 294 - <ButtonPrimary fullWidthOnMobile> 295 - <UpdateSmall /> Send an Update 343 + <ButtonPrimary fullWidth className="mb-2"> 344 + <UpdateSmall /> Send a Text Blast 296 345 </ButtonPrimary> 297 346 } 298 347 > 299 - <div className="rsvpMessageComposer flex flex-col gap-2 w-auto max-w-md"> 300 - <label className="flex flex-col font-bold text-secondary"> 301 - <p>Send a Text Blast</p> 302 - <small className="font-normal text-secondary"> 303 - Send a short text message to everyone who is <b>Going</b> or a{" "} 304 - <b>Maybe</b>. 305 - </small> 348 + <div className="rsvpMessageComposer flex flex-col gap-2 w-[1000px] max-w-full sm:max-w-md"> 349 + <div className="flex flex-col font-bold text-secondary"> 350 + <h3>Send a Text Blast to</h3> 351 + <RecipientPicker 352 + checked={checkedRecipients} 353 + setChecked={setCheckedRecipients} 354 + /> 306 355 307 356 <textarea 308 357 id="rsvp-message-input" ··· 310 359 onChange={(e) => { 311 360 setInput(e.target.value); 312 361 }} 313 - className="input-with-border w-full h-[150px] mt-1 pt-0.5 font-normal text-primary" 362 + className="input-with-border w-full h-[150px] mt-3 pt-0.5 font-normal text-primary" 314 363 /> 315 - </label> 364 + </div> 316 365 <div className="flex justify-between items-start"> 317 366 <div 318 367 className={`rsvpMessageCharCounter text-sm text-tertiary`} 319 368 style={ 320 369 input.length > 300 321 370 ? { 322 - color: theme.colors["accent-contrast"], 371 + color: theme.colors["accent-1"], 323 372 fontWeight: "bold", 324 373 } 325 374 : { ··· 333 382 disabled={input.length > 300} 334 383 className="place-self-end " 335 384 onClick={async () => { 336 - if (!permission_token) return; 385 + if (!permission_token || !publishLink) return; 337 386 await sendUpdateToRSVPS(permission_token, { 338 387 entity: props.entityID, 339 388 message: input, 340 - eventName: "Idk still figuring this out", 389 + eventName: document.title, 390 + sendto: checkedRecipients, 391 + publicLeafletID: publishLink, 341 392 }); 342 393 toaster({ 343 394 content: <div className="font-bold">Update sent!</div>, ··· 346 397 setOpen(false); 347 398 }} 348 399 > 349 - Send 400 + Text {allRecipients} {allRecipients === 1 ? "Person" : "People"}! 350 401 </ButtonPrimary> 351 402 </div> 352 403 </div> 353 404 </Popover> 354 405 ); 355 406 } 407 + 408 + const RecipientPicker = (props: { 409 + checked: { GOING: boolean; MAYBE: boolean; NOT_GOING: boolean }; 410 + setChecked: (checked: { 411 + GOING: boolean; 412 + MAYBE: boolean; 413 + NOT_GOING: boolean; 414 + }) => void; 415 + }) => { 416 + return ( 417 + <div className="flex flex-col gap-0.5"> 418 + {/* <small className="font-normal"> 419 + Send a text to everyone who RSVP&apos;d: 420 + </small> */} 421 + <div className="flex gap-4 text-secondary"> 422 + <Checkbox 423 + className="!w-fit" 424 + checked={props.checked.GOING} 425 + onChange={() => { 426 + props.setChecked({ 427 + ...props.checked, // Spread the existing values 428 + GOING: !props.checked.GOING, 429 + }); 430 + }} 431 + > 432 + Going 433 + </Checkbox> 434 + <Checkbox 435 + className="!w-fit" 436 + checked={props.checked.MAYBE} 437 + onChange={() => { 438 + props.setChecked({ 439 + ...props.checked, // Spread the existing values 440 + MAYBE: !props.checked.MAYBE, 441 + }); 442 + }} 443 + > 444 + Maybe 445 + </Checkbox> 446 + <Checkbox 447 + className="!w-fit" 448 + checked={props.checked.NOT_GOING} 449 + onChange={() => { 450 + props.setChecked({ 451 + ...props.checked, // Spread the existing values 452 + NOT_GOING: !props.checked.NOT_GOING, 453 + }); 454 + }} 455 + > 456 + Can&apos;t Go 457 + </Checkbox> 458 + </div> 459 + </div> 460 + ); 461 + }; 356 462 357 463 export let useRSVPNameState = create( 358 464 persist(
+5 -1
components/Blocks/useBlockKeyboardHandlers.ts
··· 137 137 } 138 138 139 139 // if the block is a card or mailbox... 140 - if (props.type === "card" || props.type === "mailbox") { 140 + if ( 141 + props.type === "card" || 142 + props.type === "mailbox" || 143 + props.type === "rsvp" 144 + ) { 141 145 // ...and areYouSure state is false, set it to true 142 146 if (!areYouSure) { 143 147 setAreYouSure(true);
+14 -3
components/Buttons.tsx
··· 49 49 50 50 export const ButtonSecondary = forwardRef< 51 51 HTMLButtonElement, 52 - { 52 + ButtonProps & { 53 53 fullWidth?: boolean; 54 + fullWidthOnMobile?: boolean; 54 55 children: React.ReactNode; 55 56 compact?: boolean; 56 - } & ButtonProps 57 + } 57 58 >((props, ref) => { 59 + let { 60 + className, 61 + fullWidth, 62 + fullWidthOnMobile, 63 + compact, 64 + children, 65 + ...buttonProps 66 + } = props; 58 67 return ( 59 68 <button 60 69 {...props} 61 70 ref={ref} 62 - className={`m-0 h-max ${props.fullWidth ? "w-full" : "w-max"} ${props.compact ? "py-0 px-1" : "px-2 py-0.5 "} 71 + className={`m-0 h-max 72 + ${fullWidth ? "w-full" : fullWidthOnMobile ? "w-full sm:w-max" : "w-max"} 73 + ${props.compact ? "py-0 px-1" : "px-2 py-0.5 "} 63 74 bg-bg-page outline-transparent 64 75 rounded-md text-base font-bold text-accent-contrast 65 76 flex gap-2 items-center justify-center shrink-0
+5 -5
components/Checkbox.tsx
··· 4 4 checked: boolean; 5 5 onChange: (e: React.ChangeEvent<HTMLInputElement>) => void; 6 6 children: React.ReactNode; 7 - checkboxEmptyClassName?: string; 8 - checkboxCheckedClassName?: string; 7 + className?: string; 8 + small?: boolean; 9 9 }) { 10 10 return ( 11 11 <label 12 - className={`flex w-full gap-2 items-start cursor-pointer ${props.checked ? "text-primary font-bold " : " text-tertiary font-normal"}`} 12 + className={`flex w-full gap-2 items-start cursor-pointer ${props.className} ${props.checked ? "text-primary font-bold " : " text-tertiary font-normal"}`} 13 13 > 14 14 <input 15 15 type="checkbox" ··· 19 19 /> 20 20 {!props.checked ? ( 21 21 <CheckboxEmpty 22 - className={`shrink-0 mt-[6px] text-tertiary ${props.checkboxEmptyClassName}`} 22 + className={`shrink-0 text-tertiary ${props.small ? "mt-1" : "mt-[6px]"}`} 23 23 /> 24 24 ) : ( 25 25 <CheckboxChecked 26 - className={`shrink-0 mt-[6px] text-accent-contrast ${props.checkboxCheckedClassName}`} 26 + className={`shrink-0 text-accent-contrast ${props.small ? "mt-1" : "mt-[6px]"}`} 27 27 /> 28 28 )} 29 29 {props.children}
+1 -1
components/RSVPDataProvider.tsx
··· 7 7 children: React.ReactNode; 8 8 }) { 9 9 return ( 10 - <SWRConfig value={{ fallback: { identity: props.data } }}> 10 + <SWRConfig value={{ fallback: { rsvp_data: props.data } }}> 11 11 {props.children} 12 12 </SWRConfig> 13 13 );
+6 -1
components/ThemeManager/ThemeProvider.tsx
··· 104 104 "--accent-contrast", 105 105 colorToString(accentContrast, "rgb"), 106 106 ); 107 + el?.style.setProperty( 108 + "--accent-1-is-contrast", 109 + accentContrast === accent1 ? "1" : "0", 110 + ); 107 111 }, [ 108 112 props.local, 109 113 bgLeaflet, ··· 128 132 "--accent-1": colorToString(accent1, "rgb"), 129 133 "--accent-2": colorToString(accent2, "rgb"), 130 134 "--accent-contrast": colorToString(accentContrast, "rgb"), 135 + "--accent-1-is-contrast": accentContrast === accent1 ? 1 : 0, 131 136 "--highlight-1": highlight1 132 137 ? `rgb(${colorToString(parseColor(`hsba(${highlight1.data.value})`), "rgb")})` 133 138 : "color-mix(in oklab, rgb(var(--primary)), rgb(var(--bg-page)) 75%)", ··· 216 221 ); 217 222 }; 218 223 219 - function getColorContrast(color1: string, color2: string) { 224 + export function getColorContrast(color1: string, color2: string) { 220 225 ColorSpace.register(sRGB); 221 226 222 227 let parsedColor1 = parse(`rgb(${color1})`);
+12
public/RSVPBackground/wavy.svg
··· 1 + <svg width="40" height="40" viewBox= " 50 50 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 + <path d="M10 45C10 42.2386 12.2386 40 15 40C17.7614 40 20 37.7614 20 35C20 32.2386 22.2386 30 25 30C27.7614 30 30 27.7614 30 25C30 22.2386 32.2386 20 35 20C37.7614 20 40 17.7614 40 15C40 12.2386 42.2386 10 45 10M-5 40C-2.23858 40 0 37.7614 0 35C0 32.2386 2.23858 30 5 30C5.41394 30 5.81614 29.9497 6.2008 29.8549C7.72144 29.48 8.96817 28.4094 9.58396 27M40 -5C40 -2.23858 37.7614 0 35 0C32.2386 0 30 2.23858 30 5C30 6.36273 29.4548 7.59812 28.5707 8.5M10 5C10 2.23858 12.2386 0 15 0C17.7614 0 20 -2.23858 20 -5M-5 20C-2.23858 20 0 17.7614 0 15C0 12.2386 2.23858 10 5 10M15 20C17.7614 20 20 17.7614 20 15C20 12.2386 22.2386 10 25 10M25 20C25 22.7614 22.7614 25 20 25C17.2386 25 15 27.2386 15 30C15 32.7614 12.7614 35 10 35C7.23858 35 5 37.2386 5 40C5 42.7614 2.76142 45 0 45C-2.76142 45 -5 47.2386 -5 50M-5 30C-5 27.2386 -2.76142 25 0 25C2.76142 25 5 22.7614 5 20C5 17.2386 7.23858 15 10 15C12.7614 15 15 12.7614 15 10C15 7.23858 17.2386 5 20 5C22.7614 5 25 2.76142 25 0C25 -2.76142 27.2386 -5 30 -5M10 -5C7.23858 -5 5 -2.76142 5 0C5 2.76142 2.76142 5 0 5C-2.76142 5 -5 7.23858 -5 10M30 35C27.2386 35 25 37.2386 25 40C25 42.7614 22.7614 45 20 45C17.2386 45 15 47.2386 15 50C15 52.7614 12.7614 55 10 55C7.23858 55 5 57.2386 5 60C5 62.7614 2.76142 65 0 65C-2.76142 65 -5 67.2386 -5 70M33.823 33.2227C34.5574 32.3524 35 31.2279 35 30C35 27.2386 37.2386 25 40 25C42.7614 25 45 22.7614 45 20C45 17.2386 47.2386 15 50 15C52.7614 15 55 12.7614 55 10C55 7.23858 57.2386 5 60 5C62.7614 5 65 2.76142 65 0C65 -2.76142 67.2386 -5 70 -5M11 21.9995C10.4865 22.683 10.1441 23.5023 10.0364 24.394M28.0978 15.3745C28.6842 15.1331 29.3266 15 30 15C32.7614 15 35 12.7614 35 10C35 7.23858 37.2386 5 40 5C42.7614 5 45 2.76142 45 0C45 -2.76142 47.2386 -5 50 -5M26.3277 16.6067C26.2118 16.732 26.1023 16.8633 25.9996 17M50 85C50 82.2386 52.2386 80 55 80C57.7614 80 60 77.7614 60 75C60 72.2386 62.2386 70 65 70C67.7614 70 70 67.7614 70 65C70 62.2386 72.2386 60 75 60C77.7614 60 80 57.7614 80 55C80 52.2386 82.2386 50 85 50M50 45C50 42.2386 52.2386 40 55 40C57.7614 40 60 37.7614 60 35C60 32.2386 62.2386 30 65 30C67.7614 30 70 27.7614 70 25C70 22.2386 72.2386 20 75 20C77.7614 20 80 17.7614 80 15C80 12.2386 82.2386 10 85 10M70 85C70 82.2386 72.2386 80 75 80C77.7614 80 80 77.7614 80 75C80 72.2386 82.2386 70 85 70M55 60C57.7614 60 60 57.7614 60 55C60 52.2386 62.2386 50 65 50M45 50C42.2386 50 40 52.2386 40 55C40 57.7614 37.7614 60 35 60C32.2386 60 30 62.2386 30 65C30 67.7614 27.7614 70 25 70C22.2386 70 20 72.2386 20 75C20 77.7614 17.7614 80 15 80C12.2386 80 10 82.2386 10 85M40 85C42.7614 85 45 82.7614 45 80C45 77.2386 47.2386 75 50 75C52.7614 75 55 72.7614 55 70C55 67.2386 57.2386 65 60 65C62.7614 65 65 62.7614 65 60M85 40C85 42.7614 82.7614 45 80 45C77.2386 45 75 47.2386 75 50C75 52.7614 72.7614 55 70 55C69.3266 55 68.6842 55.1331 68.0978 55.3745M70 35C67.2386 35 65 37.2386 65 40C65 42.7614 62.7614 45 60 45C57.2386 45 55 47.2386 55 50C55 52.7614 52.7614 55 50 55C47.2386 55 45 57.2386 45 60C45 62.7614 42.7614 65 40 65C37.2386 65 35 67.2386 35 70C35 71.2279 34.5574 72.3524 33.823 73.2227M60 85C62.7614 85 65 82.7614 65 80C65 77.2386 67.2386 75 70 75M85 60C85 62.7614 82.7614 65 80 65C77.2386 65 75 67.2386 75 70C75 71.2279 74.5574 72.3524 73.823 73.2227M51 61.9995C50.4865 62.683 50.1441 63.5023 50.0364 64.394M66.3277 56.6067C66.2118 56.732 66.1023 56.8633 65.9996 57M68.5707 48.5C69.4548 47.5981 70 46.3627 70 45C70 42.2386 72.2386 40 75 40C77.7614 40 80 37.7614 80 35C80 32.2386 82.2386 30 85 30M49.584 67C48.9682 68.4094 47.7214 69.48 46.2008 69.8549C45.8161 69.9497 45.4139 70 45 70C42.2386 70 40 72.2386 40 75C40 77.7614 37.7614 80 35 80C32.2386 80 30 82.2386 30 85M80 -5C80 -2.23858 77.7614 0 75 0C72.2386 0 70 2.23858 70 5C70 6.36273 69.4548 7.59812 68.5707 8.5M50 5C50 2.23858 52.2386 0 55 0C57.7614 0 60 -2.23858 60 -5M55 20C57.7614 20 60 17.7614 60 15C60 12.2386 62.2386 10 65 10M65 20C65 22.7614 62.7614 25 60 25C57.2386 25 55 27.2386 55 30C55 32.7614 52.7614 35 50 35C47.2386 35 45 37.2386 45 40C45 42.7614 42.7614 45 40 45C37.2386 45 35 47.2386 35 50C35 52.7614 32.7614 55 30 55C29.3266 55 28.6842 55.1331 28.0978 55.3745M85 0C85 2.76142 82.7614 5 80 5C77.2386 5 75 7.23858 75 10C75 12.7614 72.7614 15 70 15C69.3266 15 68.6842 15.1331 68.0978 15.3745M85 20C85 22.7614 82.7614 25 80 25C77.2386 25 75 27.2386 75 30C75 31.2279 74.5574 32.3524 73.823 33.2227M51 21.9995C50.4865 22.683 50.1441 23.5023 50.0364 24.394M66.3277 16.6067C66.2118 16.732 66.1023 16.8633 65.9996 17M49.584 27C48.9682 28.4094 47.7214 29.48 46.2008 29.8549C45.8161 29.9497 45.4139 30 45 30C42.2386 30 40 32.2386 40 35C40 37.7614 37.7614 40 35 40C32.2386 40 30 42.2386 30 45C30 46.3627 29.4548 47.5981 28.5707 48.5M-5 80C-2.23858 80 0 77.7614 0 75C0 72.2386 2.23858 70 5 70C5.41394 70 5.81614 69.9497 6.2008 69.8549C7.72144 69.48 8.96817 68.4094 9.58396 67M-5 60C-2.23858 60 0 57.7614 0 55C0 52.2386 2.23858 50 5 50M15 60C17.7614 60 20 57.7614 20 55C20 52.2386 22.2386 50 25 50M0 85C2.76142 85 5 82.7614 5 80C5 77.2386 7.23858 75 10 75C12.7614 75 15 72.7614 15 70C15 67.2386 17.2386 65 20 65C22.7614 65 25 62.7614 25 60M20 85C22.7614 85 25 82.7614 25 80C25 77.2386 27.2386 75 30 75M11 61.9995C10.4865 62.683 10.1441 63.5023 10.0364 64.394M26.3277 56.6067C26.2118 56.732 26.1023 56.8633 25.9996 57" stroke="black" stroke-width="1.5" stroke-linecap="round"> 3 + <animateTransform 4 + attributeName="transform" 5 + attributeType="XML" 6 + type="translate" 7 + from=" 50 10 " 8 + to="10 50" 9 + dur="7s" 10 + repeatCount="indefinite" /> 11 + </path> 12 + </svg>
+1 -1
src/hooks/useRSVPData.ts
··· 4 4 5 5 export function useRSVPData() { 6 6 let { permission_token } = useReplicache(); 7 - return useSWR(`identity`, () => 7 + return useSWR(`rsvp_data`, () => 8 8 getRSVPData( 9 9 permission_token.permission_token_rights.map((pr) => pr.entity_set), 10 10 ),