a tool for shared writing and social publishing
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'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);