diff --git a/actions/getRSVPData.ts b/actions/getRSVPData.ts deleted file mode 100644 index a00b3576..00000000 --- a/actions/getRSVPData.ts +++ /dev/null @@ -1,61 +0,0 @@ -"use server"; - -import { cookies } from "next/headers"; -import { supabaseServerClient } from "supabase/serverClient"; - -export async function getRSVPData(entity_sets: string[]) { - const token = (await cookies()).get("phone_auth_token"); - - let authToken: { - id: string; - created_at: string; - confirmed: boolean; - confirmation_code: string; - phone_number: string; - country_code: string; - } | null = null; - if (token) { - let { data } = await supabaseServerClient - .from("phone_number_auth_tokens") - .select("*") - .eq("id", token.value) - .single(); - authToken = data; - } - - const { data: rsvps } = await supabaseServerClient - .from("phone_rsvps_to_entity") - .select( - ` - *, - entities!inner(*) - `, - ) - .in("entities.set", entity_sets); - - return { - authToken, - rsvps: - rsvps?.map((rsvp) => { - if ( - rsvp.phone_number === authToken?.phone_number && - rsvp.country_code === authToken.country_code - ) - return { - phone_number: rsvp.phone_number, - country_code: rsvp.country_code, - name: rsvp.name, - entity: rsvp.entities.id, - status: rsvp.status, - plus_ones: rsvp.plus_ones, - }; - else - return { - name: rsvp.name, - entity: rsvp.entities.id, - status: rsvp.status, - plus_ones: rsvp.plus_ones, - }; - }) || [], - }; -} diff --git a/actions/get_phone_rsvp_to_event_state.ts b/actions/get_phone_rsvp_to_event_state.ts deleted file mode 100644 index 3f96c37f..00000000 --- a/actions/get_phone_rsvp_to_event_state.ts +++ /dev/null @@ -1,46 +0,0 @@ -"use server"; - -import { drizzle } from "drizzle-orm/node-postgres"; -import { and, eq } from "drizzle-orm"; -import postgres from "postgres"; -import { - phone_number_auth_tokens, - phone_rsvps_to_entity, -} from "drizzle/schema"; -import { cookies } from "next/headers"; -import { Database } from "supabase/database.types"; -import { pool } from "supabase/pool"; - -export async function getPhoneRSVPToEventState(entityId: string) { - const token = (await cookies()).get("phone_auth_token"); - - if (!token) { - return null; - } - - const client = await pool.connect(); - const db = drizzle(client); - - const [authToken] = await db - .select() - .from(phone_number_auth_tokens) - .where(eq(phone_number_auth_tokens.id, token.value)); - - if (!authToken || !authToken.confirmed) { - client.release(); - return null; - } - - const [rsvp] = await db - .select() - .from(phone_rsvps_to_entity) - .where( - and( - eq(phone_rsvps_to_entity.phone_number, authToken.phone_number), - eq(phone_rsvps_to_entity.entity, entityId), - ), - ); - - client.release(); - return rsvp; -} diff --git a/actions/phone_auth/confirm_phone_auth_token.ts b/actions/phone_auth/confirm_phone_auth_token.ts deleted file mode 100644 index 21a42816..00000000 --- a/actions/phone_auth/confirm_phone_auth_token.ts +++ /dev/null @@ -1,56 +0,0 @@ -"use server"; - -import { drizzle } from "drizzle-orm/node-postgres"; -import { and, eq } from "drizzle-orm"; -import postgres from "postgres"; -import { phone_number_auth_tokens } from "drizzle/schema"; -import { cookies } from "next/headers"; -import { pool } from "supabase/pool"; - -export async function confirmPhoneAuthToken(tokenId: string, code: string) { - const client = await pool.connect(); - const db = drizzle(client); - - const [token] = await db - .select() - .from(phone_number_auth_tokens) - .where(eq(phone_number_auth_tokens.id, tokenId)); - - if (!token) { - client.release(); - throw new Error("Invalid token"); - } - - if (token.confirmation_code !== code) { - client.release(); - throw new Error("Invalid confirmation code"); - } - - if (token.confirmed) { - client.release(); - throw new Error("Token already confirmed"); - } - - const [confirmedToken] = await db - .update(phone_number_auth_tokens) - .set({ - confirmed: true, - }) - .where( - and( - eq(phone_number_auth_tokens.id, tokenId), - eq(phone_number_auth_tokens.confirmation_code, code), - ), - ) - .returning(); - - (await cookies()).set("phone_auth_token", confirmedToken.id, { - maxAge: 60 * 60 * 24 * 30, - secure: process.env.NODE_ENV === "production", - httpOnly: true, - sameSite: "strict", - }); - - client.release(); - return confirmedToken; -} diff --git a/actions/phone_auth/request_phone_auth_token.ts b/actions/phone_auth/request_phone_auth_token.ts deleted file mode 100644 index 49ccc33b..00000000 --- a/actions/phone_auth/request_phone_auth_token.ts +++ /dev/null @@ -1,71 +0,0 @@ -"use server"; - -import { randomBytes } from "crypto"; -import { drizzle } from "drizzle-orm/node-postgres"; -import postgres from "postgres"; -import { phone_number_auth_tokens } from "drizzle/schema"; -import twilio from "twilio"; -import { pool } from "supabase/pool"; - -async function sendAuthCode({ - country_code, - phone_number, - code, -}: { - country_code: string; - phone_number: string; - code: string; -}) { - let phoneNumber = `+${country_code}${phone_number}`; - const accountSid = process.env.TWILIO_ACCOUNT_SID; - const authToken = process.env.TWILIO_AUTH_TOKEN; - const client = twilio(accountSid, authToken); - if (country_code === "1") { - const message = await client.messages.create({ - body: `${code} is your verification code - -@leaflet.pub #${code}`, - from: `+18449523391`, - to: phoneNumber, - }); - console.log(message); - } else { - const message = await client.messages.create({ - contentSid: "HX5ebfae4d2a423808486e773e8a22488d", - contentVariables: JSON.stringify({ 1: code }), - from: "whatsapp:+18449523391", - messagingServiceSid: "MGffbf9a66770350b25caf3b80b9aac481", - to: `whatsapp:${phoneNumber}`, - }); - } -} - -export async function createPhoneAuthToken({ - phone_number, - country_code, -}: { - phone_number: string; - country_code: string; -}) { - const client = await pool.connect(); - const db = drizzle(client); - - const code = randomBytes(3).toString("hex").toUpperCase(); - - const [token] = await db - .insert(phone_number_auth_tokens) - .values({ - phone_number, - country_code, - confirmation_code: code, - confirmed: false, - }) - .returning({ - id: phone_number_auth_tokens.id, - }); - - await sendAuthCode({ country_code, phone_number, code }); - - client.release(); - return token.id; -} diff --git a/actions/phone_rsvp_to_event.ts b/actions/phone_rsvp_to_event.ts deleted file mode 100644 index 17d6e2bd..00000000 --- a/actions/phone_rsvp_to_event.ts +++ /dev/null @@ -1,64 +0,0 @@ -"use server"; - -import { drizzle } from "drizzle-orm/node-postgres"; -import { - entities, - phone_number_auth_tokens, - phone_rsvps_to_entity, -} from "drizzle/schema"; -import { redirect } from "next/navigation"; -import postgres from "postgres"; -import { v7 } from "uuid"; -import { eq, sql } from "drizzle-orm"; -import { Database } from "supabase/database.types"; -import { createServerClient } from "@supabase/ssr"; -import { cookies } from "next/headers"; -import { pool } from "supabase/pool"; - -export async function submitRSVP(args: { - entity: string; - status: Database["public"]["Enums"]["rsvp_status"]; - name: string; - plus_ones: number; -}) { - const client = await pool.connect(); - const db = drizzle(client); - let token = (await cookies()).get("phone_auth_token"); - if (!token) throw new Error("No auth token found"); - - let [auth_token] = await db - .select() - .from(phone_number_auth_tokens) - .where(eq(phone_number_auth_tokens.id, token.value)); - if (!auth_token) throw new Error("Invalid auth token"); - if (!auth_token.confirmed) throw new Error("Auth token not confirmed"); - - await db.transaction(async (tx) => { - await tx - .insert(phone_rsvps_to_entity) - .values([ - { - status: args.status, - entity: args.entity, - phone_number: auth_token.phone_number, - country_code: auth_token.country_code, - name: args.name, - plus_ones: args.plus_ones, - }, - ]) - .onConflictDoUpdate({ - target: [ - phone_rsvps_to_entity.entity, - phone_rsvps_to_entity.phone_number, - ], - set: { - name: args.name, - status: args.status, - plus_ones: args.plus_ones, - }, - }); - }); - - client.release(); - return { success: true }; -} diff --git a/actions/sendUpdateToRSVPS.ts b/actions/sendUpdateToRSVPS.ts deleted file mode 100644 index 2b1d429b..00000000 --- a/actions/sendUpdateToRSVPS.ts +++ /dev/null @@ -1,71 +0,0 @@ -"use server"; -import { drizzle } from "drizzle-orm/node-postgres"; -import { eq } from "drizzle-orm"; -import { - entities, - permission_token_rights, - phone_rsvps_to_entity, -} from "drizzle/schema"; -import twilio from "twilio"; -import { pool } from "supabase/pool"; - -export async function sendUpdateToRSVPS( - token: { id: string }, - { - entity, - message, - eventName, - sendto, - publicLeafletID, - }: { - entity: string; - message: string; - eventName: string; - publicLeafletID: string; - sendto: { GOING: boolean; MAYBE: boolean; NOT_GOING: boolean }; - }, -) { - let dbclient = await pool.connect(); - const db = drizzle(dbclient); - let token_rights = await db - .select() - .from(permission_token_rights) - .where(eq(permission_token_rights.token, token.id)); - - let RSVPS = db - .select() - .from(phone_rsvps_to_entity) - .innerJoin(entities, eq(phone_rsvps_to_entity.entity, entities.id)) - .where(eq(phone_rsvps_to_entity.entity, entity)); - - dbclient.release(); - - if (!token_rights[0]?.write) return; - let rsvps = await RSVPS; - let entity_set = rsvps[0]?.entities.set; - if (!token_rights.find((r) => r.entity_set === entity_set)) { - return; - } - - const accountSid = process.env.TWILIO_ACCOUNT_SID; - const authToken = process.env.TWILIO_AUTH_TOKEN; - const client = twilio(accountSid, authToken); - - for (let rsvp of rsvps) { - if (sendto[rsvp.phone_rsvps_to_entity.status]) { - let { country_code, phone_number } = rsvp.phone_rsvps_to_entity; - let number = `+${country_code}${phone_number}`; - await client.messages.create({ - contentSid: "HX8e1217f791d38fa4cf7b7b24a02fe10c", - contentVariables: JSON.stringify({ - 1: eventName, - 2: message, - 3: `https://leaflet.pub/${publicLeafletID}`, - }), - from: `${country_code === "1" ? "" : "whatsapp:"}+18449523391`, - messagingServiceSid: "MGffbf9a66770350b25caf3b80b9aac481", - to: country_code === "1" ? number : `whatsapp:${number}`, - }); - } - } -} diff --git a/app/[leaflet_id]/page.tsx b/app/[leaflet_id]/page.tsx index 851ac894..5a90a3b5 100644 --- a/app/[leaflet_id]/page.tsx +++ b/app/[leaflet_id]/page.tsx @@ -7,7 +7,7 @@ import type { Attribute } from "src/replicache/attributes"; import { YJSFragmentToString } from "src/utils/yjsFragmentToString"; import { Leaflet } from "./Leaflet"; import { scanIndexLocal } from "src/replicache/utils"; -import { getRSVPData } from "actions/getRSVPData"; + import { PageSWRDataProvider } from "components/PageSWRDataProvider"; import { getPollData } from "actions/pollActions"; import { supabaseServerClient } from "supabase/serverClient"; @@ -41,24 +41,25 @@ export default async function LeafletPage(props: Props) { ); - let [{ data }, rsvp_data, poll_data] = await Promise.all([ + let [{ data }, poll_data] = await Promise.all([ supabaseServerClient.rpc("get_facts", { root: rootEntity, }), - getRSVPData(res.data.permission_token_rights.map((ptr) => ptr.entity_set)), getPollData(res.data.permission_token_rights.map((ptr) => ptr.entity_set)), ]); let initialFacts = (data as unknown as Fact[]) || []; // Extract font settings from facts for server-side font loading - const { headingFontId, bodyFontId } = extractFontsFromFacts(initialFacts as any, rootEntity); + const { headingFontId, bodyFontId } = extractFontsFromFacts( + initialFacts as any, + rootEntity, + ); return ( <> {/* Server-side font loading with preload and @font-face */} rsvp.entity === props.entityID) || []; - let going = attendees.filter((rsvp) => rsvp.status === "GOING"); - let maybe = attendees.filter((rsvp) => rsvp.status === "MAYBE"); - let notGoing = attendees.filter((rsvp) => rsvp.status === "NOT_GOING"); - - return ( - - No RSVPs yet - - ) : ( - - {going.length > 0 && - `${going.reduce((acc, g) => acc + 1 + g.plus_ones, 0)} Going`} - {maybe.length > 0 && - `${going.length > 0 ? ", " : ""}${maybe.reduce((acc, m) => acc + 1 + m.plus_ones, 0)} Maybe`} - - ) - } - > - {going.length === 0 && maybe.length === 0 && notGoing.length === 0 && ( -
No RSVPs yet
- )} - - - -
- ); -} - -function AttendeeStatusList(props: { - rsvps: Array<{ - name: string; - phone_number?: string; - plus_ones: number; - status: string; - }>; - title: string; -}) { - if (props.rsvps.length === 0) return null; - return ( -
-
- {props.title} ({props.rsvps.length}) -
- {props.rsvps.map((rsvp) => ( -
- {rsvp.name} {rsvp.plus_ones > 0 ? `+${rsvp.plus_ones}` : ""} -
- ))} -
- ); -} diff --git a/components/Blocks/RSVPBlock/ContactDetailsForm.tsx b/components/Blocks/RSVPBlock/ContactDetailsForm.tsx deleted file mode 100644 index a1a01222..00000000 --- a/components/Blocks/RSVPBlock/ContactDetailsForm.tsx +++ /dev/null @@ -1,366 +0,0 @@ -"use client"; -import { useSmoker, useToaster } from "components/Toast"; -import { RSVP_Status, RSVPButtons, State, useRSVPNameState } from "."; -import { createContext, useContext, useState } from "react"; -import { useRSVPData } from "components/PageSWRDataProvider"; -import { confirmPhoneAuthToken } from "actions/phone_auth/confirm_phone_auth_token"; -import { submitRSVP } from "actions/phone_rsvp_to_event"; - -import { countryCodes } from "src/constants/countryCodes"; -import { Checkbox } from "components/Checkbox"; -import { ButtonPrimary, ButtonTertiary } from "components/Buttons"; -import { Separator } from "components/Layout"; -import { createPhoneAuthToken } from "actions/phone_auth/request_phone_auth_token"; -import { Input, InputWithLabel } from "components/Input"; -import { RequestHeadersContext } from "components/Providers/RequestHeadersProvider"; -import { Popover } from "components/Popover"; -import { theme } from "tailwind.config"; -import { InfoSmall } from "components/Icons/InfoSmall"; - -export function ContactDetailsForm(props: { - status: RSVP_Status; - entityID: string; - setState: (s: State) => void; - setStatus: (s: RSVP_Status) => void; -}) { - let { status, entityID, setState, setStatus } = props; - let focusWithinStyles = - "focus-within:border-tertiary focus-within:outline-solid focus-within:outline-2 focus-within:outline-tertiary focus-within:outline-offset-1"; - let toaster = useToaster(); - let { data, mutate } = useRSVPData(); - let [contactFormState, setContactFormState] = useState< - { state: "details" } | { state: "confirm"; token: string } - >({ state: "details" }); - let { name, setName } = useRSVPNameState(); - let [plus_ones, setPlusOnes] = useState( - data?.rsvps?.find( - (rsvp) => - data.authToken && - rsvp.entity === props.entityID && - data.authToken.country_code === rsvp.country_code && - data.authToken.phone_number === rsvp.phone_number, - )?.plus_ones || 0, - ); - let requestHeaders = useContext(RequestHeadersContext); - const [formState, setFormState] = useState({ - country_code: - countryCodes.find((c) => c[1].toUpperCase() === (requestHeaders.country || "US"))?.[2] || "1", - phone_number: "", - confirmationCode: "", - }); - - let submit = async ( - token: Awaited>, - ) => { - try { - await submitRSVP({ - status, - name: name, - entity: entityID, - plus_ones, - }); - } catch (e) { - //handle failed confirm - return false; - } - - mutate({ - authToken: token, - rsvps: [ - ...(data?.rsvps || []).filter((r) => r.entity !== entityID), - { - name: name, - status, - plus_ones, - entity: entityID, - phone_number: token.phone_number, - country_code: token.country_code, - }, - ], - }); - props.setState({ state: "default" }); - return true; - }; - return contactFormState.state === "details" ? ( - <> -
{ - e.preventDefault(); - if (data?.authToken) { - submit(data.authToken); - toaster({ - content: ( -
- {status === "GOING" - ? "Yay! You're Going!" - : status === "MAYBE" - ? "You're a Maybe" - : "Sorry you can't make it D:"} -
- ), - type: "success", - }); - } else { - let tokenId = await createPhoneAuthToken(formState); - setContactFormState({ state: "confirm", token: tokenId }); - } - }} - > - - -
- -
- -
- {formState.country_code !== "1" ? ( - <> - Messages to non-US/Canada numbers will be sent via{" "} - WhatsApp - - ) : null} -
-
-
- setPlusOnes(parseInt(e.currentTarget.value))} - onKeyDown={(e) => { - if (e.key === "Backspace" && !e.currentTarget.value) - e.preventDefault(); - }} - /> -
-
- -
-
- - { - setState({ state: "default" }); - }} - > - Back - - - RSVP as{" "} - {status === "GOING" - ? "Going" - : status === "MAYBE" - ? "Maybe" - : "Can't Go"} - -
- - - ) : ( - - setFormState((state) => ({ ...state, confirmationCode: value })) - } - /> - ); -} - -const ConfirmationForm = (props: { - country_code: string; - phoneNumber: string; - value: string; - token: string; - status: RSVP_Status; - submit: ( - token: Awaited>, - ) => Promise; - onChange: (v: string) => void; -}) => { - let smoker = useSmoker(); - let toaster = useToaster(); - return ( -
{ - e.preventDefault(); - let rect = document - .getElementById("rsvp-code-confirm-button") - ?.getBoundingClientRect(); - try { - let token = await confirmPhoneAuthToken(props.token, props.value); - props.submit(token); - toaster({ - content: ( -
- {props.status === "GOING" - ? "Yay! You're Going!" - : props.status === "MAYBE" - ? "You're a Maybe" - : "Sorry you can't make it D:"} -
- ), - type: "success", - }); - } catch (error) { - smoker({ - alignOnMobile: "left", - error: true, - text: "invalid code!", - position: { - x: rect ? rect.left + (rect.right - rect.left) / 2 : 0, - y: rect ? rect.top + 26 : 0, - }, - }); - return; - } - }} - > - - - - Confirm - -
- ); -}; - -const ConsentPopover = (props: { country_code: string }) => { - return ( - }> -
- By RSVPing I to consent to receive - {props.country_code === "1" ? "" : " WhatsApp"} messages from the event - host, via Leaflet! -
-
- ); -}; diff --git a/components/Blocks/RSVPBlock/RSVPBackground.module.css b/components/Blocks/RSVPBlock/RSVPBackground.module.css deleted file mode 100644 index 93dcd918..00000000 --- a/components/Blocks/RSVPBlock/RSVPBackground.module.css +++ /dev/null @@ -1,7 +0,0 @@ -.RSVPWavyBG { - opacity: calc(1 - (var(--accent-1-is-contrast) * 0.6)); - mask-image: url("/RSVPBackground/wavy.svg"); - mask-repeat: repeat repeat; - mask-position: center; - mask-size: 64px; -} diff --git a/components/Blocks/RSVPBlock/SendUpdate.tsx b/components/Blocks/RSVPBlock/SendUpdate.tsx deleted file mode 100644 index fb13b7f6..00000000 --- a/components/Blocks/RSVPBlock/SendUpdate.tsx +++ /dev/null @@ -1,194 +0,0 @@ -"use client"; -import { useState } from "react"; -import { useRSVPData } from "components/PageSWRDataProvider"; -import { useEntitySetContext } from "components/EntitySetProvider"; -import { ButtonPrimary } from "components/Buttons"; -import { Popover } from "components/Popover"; -import { theme } from "tailwind.config"; -import { useToaster } from "components/Toast"; -import { sendUpdateToRSVPS } from "actions/sendUpdateToRSVPS"; -import { useReplicache } from "src/replicache"; -import { Checkbox } from "components/Checkbox"; -import { useReadOnlyShareLink } from "app/[leaflet_id]/actions/ShareOptions"; - -export function SendUpdateButton(props: { entityID: string }) { - let publishLink = useReadOnlyShareLink(); - let { permissions } = useEntitySetContext(); - let { permission_token } = useReplicache(); - let [input, setInput] = useState(""); - let toaster = useToaster(); - let [open, setOpen] = useState(false); - let [checkedRecipients, setCheckedRecipients] = useState({ - GOING: true, - MAYBE: true, - NOT_GOING: false, - }); - - let { data, mutate } = useRSVPData(); - let attendees = - data?.rsvps?.filter((rsvp) => rsvp.entity === props.entityID) || []; - let going = attendees.filter((rsvp) => rsvp.status === "GOING"); - let maybe = attendees.filter((rsvp) => rsvp.status === "MAYBE"); - let notGoing = attendees.filter((rsvp) => rsvp.status === "NOT_GOING"); - - let allRecipients = - ((checkedRecipients.GOING && going.length) || 0) + - ((checkedRecipients.MAYBE && maybe.length) || 0) + - ((checkedRecipients.NOT_GOING && notGoing.length) || 0); - - if (!!!permissions.write) return; - return ( - setOpen(open)} - trigger={ - - Send a Text Blast - - } - > -
-
-

Send a Text Blast to

- - -