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