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(
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'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);