a tool for shared writing and social publishing

Add RSVP blocks with whatsapp messaging

Squashed commit of the following:

commit 5bed2bbd67dc76959aa4b5378fb83132bef7a986
Author: Jared Pereira <jared@awarm.space>
Date: Tue Jan 7 21:02:29 2025 -0500

store country code and base number seperately in db

commit 6b7754375008cd1ad367fd5095443562a094114b
Author: Jared Pereira <jared@awarm.space>
Date: Tue Jan 7 16:27:39 2025 -0500

move migration up

commit 0ff4f2c33f444f7c2ee9ec69a3f0223bef285554
Author: Jared Pereira <jared@awarm.space>
Date: Tue Jan 7 16:11:03 2025 -0500

fix disabled button check

commit c5cf6a6ed623f1cf7eb20f1c88f5e696d938452e
Author: Jared Pereira <jared@awarm.space>
Date: Tue Jan 7 15:31:50 2025 -0500

remove unusued imports

commit a84414e25d2c888e867c686abf0b25638a025c74
Author: Jared Pereira <jared@awarm.space>
Date: Tue Jan 7 15:07:47 2025 -0500

add country code picker and get default from ip

commit 1bbd2ebbdff3c50bec4effe620ad5a10ebe7a951
Merge: 3e51a3e 7d0a5bd
Author: Jared Pereira <jared@awarm.space>
Date: Mon Jan 6 17:45:41 2025 -0500

Merge branch 'main' into feature/rsvp-blocks-2

commit 3e51a3e12c52e08d747fc0e10b32e35649fd3e2e
Author: celine <celine@hyperlink.academy>
Date: Mon Dec 16 14:16:00 2024 -0500

checkbox width bug fixed

commit ee9f56e3ce951a4e634da757910c6597a5bbb6c2
Author: celine <celine@hyperlink.academy>
Date: Mon Dec 16 14:12:20 2024 -0500

button is disabled if checkbox is not checked

commit 7d6e90e1a3e40168d3d91a01b5510db3016c00e6
Author: celine <celine@hyperlink.academy>
Date: Mon Dec 16 14:07:38 2024 -0500

added more explicit consent form

commit 744a36d011b48f2ff293612f7e1f1bdc2433295c
Author: Jared Pereira <jared@awarm.space>
Date: Thu Dec 12 00:37:12 2024 -0500

add whatsapp sending!

commit 360b295bb3b11d8c08b472919aa48be0fa05efa8
Author: celine <celine@hyperlink.academy>
Date: Mon Dec 9 21:56:17 2024 -0500

added a success state to the message composer

commit 158eb0d7f56d8ee7f0acda3fb47c5a1dbb7a358e
Author: celine <celine@hyperlink.academy>
Date: Mon Dec 9 21:17:04 2024 -0500

added more styling stuff in the message composer in rsvp

commit f5d8e20387c29a208aedd49f011dcd82f38d7380
Author: celine <celine@hyperlink.academy>
Date: Thu Dec 5 19:00:55 2024 -0500

styled verify code UI, added tweak to smoker to get it to center on the mouse click

commit 7ebdfc2b27f3ee72122e9ece06fe7417223b41ee
Author: celine <celine@hyperlink.academy>
Date: Thu Dec 5 17:54:18 2024 -0500

tweaks to rsvp message composer styling

commit c1e4f359097ad430beb6040990d7633bf72fda16
Author: celine <celine@hyperlink.academy>
Date: Thu Dec 5 16:06:03 2024 -0500

styled write an update modal, tweaked popover logic to accept buttons as triggers

commit 7675e17b76a044a90a5de86aedc9f89088640d77
Merge: 66170f5 60039c6
Author: celine <celine@hyperlink.academy>
Date: Wed Dec 4 22:51:30 2024 -0500

Merge branch 'main' of https://github.com/hyperlink-academy/minilink into feature/rsvp-blocks-2

commit 66170f507da1578b2c053a3631aa5b5e534842dd
Author: celine <celine@hyperlink.academy>
Date: Wed Dec 4 17:55:23 2024 -0500

added an icon for rsvp in the block commands and also added some dividers to separate out the event stuff

commit c1cc37961ce2b150c86aa4d678a992d1f57362ff
Author: celine <celine@hyperlink.academy>
Date: Wed Dec 4 17:28:55 2024 -0500

styled the rsvp form and also added a tooltip with a consent message in it

commit d7a07cd5d46f04c384ede6fc7f08efa58649f578
Author: celine <celine@hyperlink.academy>
Date: Tue Dec 3 22:37:23 2024 -0500

inital styling for RSVP

commit ff80f740a2652f7531596972eca22838de75d1e0
Author: Jared Pereira <jared@awarm.space>
Date: Mon Dec 2 15:48:45 2024 -0500

add migration for new tables

commit 2c9951bc3421896ef0c62b6fb7bcf176ade1c111
Author: Jared Pereira <jared@awarm.space>
Date: Fri Nov 29 02:00:24 2024 -0500

add a bunch of styling and names

commit 527f324dbd56f709c66199097b5fa5cbefecc1bd
Author: Jared Pereira <jared@awarm.space>
Date: Thu Nov 28 18:59:21 2024 -0500

render attendee data properly

commit 00100e609cbc1c0d2b9cffeda9ed564bf952fe61
Author: Jared Pereira <jared@awarm.space>
Date: Wed Nov 27 23:49:48 2024 -0500

create top level identity provider

commit 48be121a4402bd59884010e0668e3e63685b853d
Author: Jared Pereira <jared@awarm.space>
Date: Wed Nov 27 21:54:20 2024 -0500

add super basic RSVP flow

commit 41e9dd9578072e5ad5a74a69175235248e0d0b42
Author: Jared Pereira <jared@awarm.space>
Date: Wed Nov 27 20:23:51 2024 -0500

WIP

commit 1d6d867b68521ecc1d641b881d8d33fab694ee34
Author: Jared Pereira <jared@awarm.space>
Date: Sun Nov 24 01:06:12 2024 -0500

add rsvp block type and dummy block

+1938 -72
+56
actions/getRSVPData.ts
··· 1 + "use server"; 2 + 3 + import { drizzle } from "drizzle-orm/postgres-js"; 4 + import { and, eq, inArray } from "drizzle-orm"; 5 + import postgres from "postgres"; 6 + import { 7 + entities, 8 + phone_number_auth_tokens, 9 + phone_rsvps_to_entity, 10 + } from "drizzle/schema"; 11 + import { cookies } from "next/headers"; 12 + 13 + export async function getRSVPData(entity_sets: string[]) { 14 + const token = cookies().get("phone_auth_token"); 15 + if (!token) { 16 + return null; 17 + } 18 + 19 + const client = postgres(process.env.DB_URL as string, { idle_timeout: 5 }); 20 + const db = drizzle(client); 21 + 22 + const [authToken] = await db 23 + .select() 24 + .from(phone_number_auth_tokens) 25 + .where(eq(phone_number_auth_tokens.id, token.value)); 26 + 27 + const rsvps = await db 28 + .select() 29 + .from(phone_rsvps_to_entity) 30 + .innerJoin(entities, eq(entities.id, phone_rsvps_to_entity.entity)) 31 + .where(and(inArray(entities.set, entity_sets))); 32 + 33 + client.end(); 34 + return { 35 + authToken, 36 + rsvps: rsvps.map((rsvp) => { 37 + if ( 38 + rsvp.phone_rsvps_to_entity.phone_number === authToken?.phone_number && 39 + rsvp.phone_rsvps_to_entity.country_code === authToken.country_code 40 + ) 41 + return { 42 + phone_number: rsvp.phone_rsvps_to_entity.phone_number, 43 + country_code: rsvp.phone_rsvps_to_entity.country_code, 44 + name: rsvp.phone_rsvps_to_entity.name, 45 + entity: rsvp.entities.id, 46 + status: rsvp.phone_rsvps_to_entity.status, 47 + }; 48 + else 49 + return { 50 + name: rsvp.phone_rsvps_to_entity.name, 51 + entity: rsvp.entities.id, 52 + status: rsvp.phone_rsvps_to_entity.status, 53 + }; 54 + }), 55 + }; 56 + }
+45
actions/get_phone_rsvp_to_event_state.ts
··· 1 + "use server"; 2 + 3 + import { drizzle } from "drizzle-orm/postgres-js"; 4 + import { and, eq } from "drizzle-orm"; 5 + import postgres from "postgres"; 6 + import { 7 + phone_number_auth_tokens, 8 + phone_rsvps_to_entity, 9 + } from "drizzle/schema"; 10 + import { cookies } from "next/headers"; 11 + import { Database } from "supabase/database.types"; 12 + 13 + export async function getPhoneRSVPToEventState(entityId: string) { 14 + const token = cookies().get("phone_auth_token"); 15 + 16 + if (!token) { 17 + return null; 18 + } 19 + 20 + const client = postgres(process.env.DB_URL as string, { idle_timeout: 5 }); 21 + const db = drizzle(client); 22 + 23 + const [authToken] = await db 24 + .select() 25 + .from(phone_number_auth_tokens) 26 + .where(eq(phone_number_auth_tokens.id, token.value)); 27 + 28 + if (!authToken || !authToken.confirmed) { 29 + client.end(); 30 + return null; 31 + } 32 + 33 + const [rsvp] = await db 34 + .select() 35 + .from(phone_rsvps_to_entity) 36 + .where( 37 + and( 38 + eq(phone_rsvps_to_entity.phone_number, authToken.phone_number), 39 + eq(phone_rsvps_to_entity.entity, entityId), 40 + ), 41 + ); 42 + 43 + client.end(); 44 + return rsvp; 45 + }
+55
actions/phone_auth/confirm_phone_auth_token.ts
··· 1 + "use server"; 2 + 3 + import { drizzle } from "drizzle-orm/postgres-js"; 4 + import { and, eq } from "drizzle-orm"; 5 + import postgres from "postgres"; 6 + import { phone_number_auth_tokens } from "drizzle/schema"; 7 + import { cookies } from "next/headers"; 8 + 9 + export async function confirmPhoneAuthToken(tokenId: string, code: string) { 10 + const client = postgres(process.env.DB_URL as string, { idle_timeout: 5 }); 11 + const db = drizzle(client); 12 + 13 + const [token] = await db 14 + .select() 15 + .from(phone_number_auth_tokens) 16 + .where(eq(phone_number_auth_tokens.id, tokenId)); 17 + 18 + if (!token) { 19 + client.end(); 20 + throw new Error("Invalid token"); 21 + } 22 + 23 + if (token.confirmation_code !== code) { 24 + client.end(); 25 + throw new Error("Invalid confirmation code"); 26 + } 27 + 28 + if (token.confirmed) { 29 + client.end(); 30 + throw new Error("Token already confirmed"); 31 + } 32 + 33 + const [confirmedToken] = await db 34 + .update(phone_number_auth_tokens) 35 + .set({ 36 + confirmed: true, 37 + }) 38 + .where( 39 + and( 40 + eq(phone_number_auth_tokens.id, tokenId), 41 + eq(phone_number_auth_tokens.confirmation_code, code), 42 + ), 43 + ) 44 + .returning(); 45 + 46 + cookies().set("phone_auth_token", confirmedToken.id, { 47 + maxAge: 60 * 60 * 24 * 30, 48 + secure: process.env.NODE_ENV === "production", 49 + httpOnly: true, 50 + sameSite: "strict", 51 + }); 52 + 53 + client.end(); 54 + return confirmedToken; 55 + }
+52
actions/phone_auth/request_phone_auth_token.ts
··· 1 + "use server"; 2 + 3 + import { randomBytes } from "crypto"; 4 + import { drizzle } from "drizzle-orm/postgres-js"; 5 + import postgres from "postgres"; 6 + import { phone_number_auth_tokens } from "drizzle/schema"; 7 + import twilio from "twilio"; 8 + 9 + async function sendAuthCode(phoneNumber: string, code: string) { 10 + const accountSid = process.env.TWILIO_ACCOUNT_SID; 11 + const authToken = process.env.TWILIO_AUTH_TOKEN; 12 + const client = twilio(accountSid, authToken); 13 + 14 + const message = await client.messages.create({ 15 + contentSid: "HX5ebfae4d2a423808486e773e8a22488d", 16 + contentVariables: JSON.stringify({ 1: code }), 17 + from: "whatsapp:+18449523391", 18 + messagingServiceSid: "MGffbf9a66770350b25caf3b80b9aac481", 19 + to: `whatsapp:${phoneNumber}`, 20 + }); 21 + console.log(message.body); 22 + } 23 + 24 + export async function createPhoneAuthToken({ 25 + phone_number, 26 + country_code, 27 + }: { 28 + phone_number: string; 29 + country_code: string; 30 + }) { 31 + const client = postgres(process.env.DB_URL as string, { idle_timeout: 5 }); 32 + const db = drizzle(client); 33 + 34 + const code = randomBytes(3).toString("hex").toUpperCase(); 35 + 36 + const [token] = await db 37 + .insert(phone_number_auth_tokens) 38 + .values({ 39 + phone_number, 40 + country_code, 41 + confirmation_code: code, 42 + confirmed: false, 43 + }) 44 + .returning({ 45 + id: phone_number_auth_tokens.id, 46 + }); 47 + 48 + await sendAuthCode(`+${country_code}${phone_number}`, code); 49 + 50 + client.end(); 51 + return token.id; 52 + }
+60
actions/phone_rsvp_to_event.ts
··· 1 + "use server"; 2 + 3 + import { drizzle } from "drizzle-orm/postgres-js"; 4 + import { 5 + entities, 6 + phone_number_auth_tokens, 7 + phone_rsvps_to_entity, 8 + } from "drizzle/schema"; 9 + import { redirect } from "next/navigation"; 10 + import postgres from "postgres"; 11 + import { v7 } from "uuid"; 12 + import { eq, sql } from "drizzle-orm"; 13 + import { Database } from "supabase/database.types"; 14 + import { createServerClient } from "@supabase/ssr"; 15 + import { cookies } from "next/headers"; 16 + 17 + export async function submitRSVP(args: { 18 + entity: string; 19 + status: Database["public"]["Enums"]["rsvp_status"]; 20 + name: string; 21 + }) { 22 + const client = postgres(process.env.DB_URL as string, { idle_timeout: 5 }); 23 + const db = drizzle(client); 24 + let token = cookies().get("phone_auth_token"); 25 + if (!token) throw new Error("No auth token found"); 26 + 27 + let [auth_token] = await db 28 + .select() 29 + .from(phone_number_auth_tokens) 30 + .where(eq(phone_number_auth_tokens.id, token.value)); 31 + if (!auth_token) throw new Error("Invalid auth token"); 32 + if (!auth_token.confirmed) throw new Error("Auth token not confirmed"); 33 + 34 + await db.transaction(async (tx) => { 35 + await tx 36 + .insert(phone_rsvps_to_entity) 37 + .values([ 38 + { 39 + status: args.status, 40 + entity: args.entity, 41 + phone_number: auth_token.phone_number, 42 + country_code: auth_token.country_code, 43 + name: args.name, 44 + }, 45 + ]) 46 + .onConflictDoUpdate({ 47 + target: [ 48 + phone_rsvps_to_entity.entity, 49 + phone_rsvps_to_entity.phone_number, 50 + ], 51 + set: { 52 + name: args.name, 53 + status: args.status, 54 + }, 55 + }); 56 + }); 57 + 58 + client.end(); 59 + return { success: true }; 60 + }
+62
actions/sendUpdateToRSVPS.ts
··· 1 + "use server"; 2 + import { drizzle } from "drizzle-orm/postgres-js"; 3 + import { eq } from "drizzle-orm"; 4 + import postgres from "postgres"; 5 + import { 6 + entities, 7 + permission_token_rights, 8 + phone_rsvps_to_entity, 9 + } from "drizzle/schema"; 10 + import { createClient } from "@supabase/supabase-js"; 11 + import { Database } from "supabase/database.types"; 12 + import twilio from "twilio"; 13 + import { Message } from "twilio/lib/twiml/MessagingResponse"; 14 + 15 + const client = postgres(process.env.DB_URL as string, { idle_timeout: 5 }); 16 + let supabase = createClient<Database>( 17 + process.env.NEXT_PUBLIC_SUPABASE_API_URL as string, 18 + process.env.SUPABASE_SERVICE_ROLE_KEY as string, 19 + ); 20 + const db = drizzle(client); 21 + 22 + export async function sendUpdateToRSVPS( 23 + token: { id: string }, 24 + { 25 + entity, 26 + message, 27 + eventName, 28 + }: { entity: string; message: string; eventName: string }, 29 + ) { 30 + let token_rights = await db 31 + .select() 32 + .from(permission_token_rights) 33 + .where(eq(permission_token_rights.token, token.id)); 34 + 35 + let RSVPS = db 36 + .select() 37 + .from(phone_rsvps_to_entity) 38 + .innerJoin(entities, eq(phone_rsvps_to_entity.entity, entities.id)) 39 + .where(eq(phone_rsvps_to_entity.entity, entity)); 40 + 41 + if (!token_rights[0]?.write) return; 42 + let rsvps = await RSVPS; 43 + let entity_set = rsvps[0]?.entities.set; 44 + if (!token_rights.find((r) => r.entity_set === entity_set)) { 45 + return; 46 + } 47 + 48 + const accountSid = process.env.TWILIO_ACCOUNT_SID; 49 + const authToken = process.env.TWILIO_AUTH_TOKEN; 50 + const client = twilio(accountSid, authToken); 51 + 52 + 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); 61 + } 62 + }
+15 -8
app/[leaflet_id]/page.tsx
··· 9 9 import { YJSFragmentToString } from "components/Blocks/TextBlock/RenderYJSFragment"; 10 10 import { Leaflet } from "./Leaflet"; 11 11 import { scanIndexLocal } from "src/replicache/utils"; 12 + import { getRSVPData } from "actions/getRSVPData"; 13 + import { RSVPDataProvider } from "components/RSVPDataProvider"; 12 14 13 15 export const preferredRegion = ["sfo1"]; 14 16 export const dynamic = "force-dynamic"; ··· 48 50 </div> 49 51 ); 50 52 51 - let { data } = await supabase.rpc("get_facts", { 52 - root: rootEntity, 53 - }); 53 + let [{ data }, identity_data] = await Promise.all([ 54 + supabase.rpc("get_facts", { 55 + root: rootEntity, 56 + }), 57 + getRSVPData(res.data.permission_token_rights.map((ptr) => ptr.entity_set)), 58 + ]); 54 59 let initialFacts = (data as unknown as Fact<keyof typeof Attributes>[]) || []; 55 60 return ( 56 - <Leaflet 57 - initialFacts={initialFacts} 58 - leaflet_id={rootEntity} 59 - token={res.data} 60 - /> 61 + <RSVPDataProvider data={identity_data}> 62 + <Leaflet 63 + initialFacts={initialFacts} 64 + leaflet_id={rootEntity} 65 + token={res.data} 66 + /> 67 + </RSVPDataProvider> 61 68 ); 62 69 } 63 70
+3
app/globals.css
··· 180 180 @apply px-1; 181 181 @apply py-0.5; 182 182 @apply hover:border-tertiary; 183 + 183 184 @apply focus:border-tertiary; 184 185 @apply focus:outline; 185 186 @apply focus:outline-tertiary; 186 187 @apply focus:outline-2; 187 188 @apply focus:outline-offset-1; 189 + 188 190 @apply focus-within:border-tertiary; 189 191 @apply focus-within:outline; 190 192 @apply focus-within:outline-tertiary; 191 193 @apply focus-within:outline-2; 192 194 @apply focus-within:outline-offset-1; 195 + 193 196 @apply disabled:border-border-light; 194 197 @apply disabled:bg-border-light; 195 198 @apply disabled:text-tertiary;
+6 -1
app/layout.tsx
··· 6 6 import localFont from "next/font/local"; 7 7 import { PopUpProvider } from "components/Toast"; 8 8 import { IdentityProviderServer } from "components/IdentityProviderServer"; 9 + import { headers } from "next/headers"; 10 + import { IPLocationProvider } from "components/Providers/IPLocationProvider"; 9 11 10 12 export const metadata = { 11 13 title: "Leaflet", ··· 44 46 }: { 45 47 children: React.ReactNode; 46 48 }) { 49 + let ipLocation = headers().get("X-Vercel-IP-Country"); 47 50 return ( 48 51 <html lang="en" className={`${quattro.variable}`}> 49 52 <body> ··· 65 68 <InitialPageLoad> 66 69 <PopUpProvider> 67 70 <IdentityProviderServer> 68 - <ViewportSizeLayout>{children}</ViewportSizeLayout> 71 + <IPLocationProvider country={ipLocation}> 72 + <ViewportSizeLayout>{children}</ViewportSizeLayout> 73 + </IPLocationProvider> 69 74 </IdentityProviderServer> 70 75 </PopUpProvider> 71 76 </InitialPageLoad>
+2
components/Blocks/Block.tsx
··· 27 27 import { Media } from "components/Media"; 28 28 import { useIsMobile } from "src/hooks/isMobile"; 29 29 import { DateTimeBlock } from "./DateTimeBlock"; 30 + import { RSVPBlock } from "./RSVPBlock"; 30 31 import { elementId } from "src/utils/elementId"; 31 32 32 33 export type Block = { ··· 160 161 embed: EmbedBlock, 161 162 mailbox: MailboxBlock, 162 163 datetime: DateTimeBlock, 164 + rsvp: RSVPBlock, 163 165 }; 164 166 165 167 export const BlockMultiselectIndicator = (props: BlockProps) => {
+8 -1
components/Blocks/BlockCommands.tsx
··· 12 12 LinkSmall, 13 13 BlockEmbedSmall, 14 14 BlockCalendarSmall, 15 + RSVPSmall, 15 16 } from "components/Icons"; 16 17 import { generateKeyBetween } from "fractional-indexing"; 17 18 import { focusPage } from "components/Pages"; ··· 198 199 // EVENT STUFF 199 200 200 201 { 202 + name: "RSVP", 203 + icon: <RSVPSmall />, 204 + type: "event", 205 + onSelect: (rep, props) => createBlockWithType(rep, props, "rsvp"), 206 + }, 207 + { 201 208 name: "Date and Time", 202 209 icon: <BlockCalendarSmall />, 203 - type: "block", 210 + type: "event", 204 211 onSelect: (rep, props) => createBlockWithType(rep, props, "datetime"), 205 212 }, 206 213
+4 -4
components/Blocks/EmbedBlock.tsx
··· 128 128 }, 129 129 }); 130 130 }; 131 - let smoke = useSmoker(); 131 + let smoker = useSmoker(); 132 132 133 133 return ( 134 134 <div> ··· 153 153 if (!linkValue) return; 154 154 if (!isUrl(linkValue)) { 155 155 let rect = e.currentTarget.getBoundingClientRect(); 156 - smoke({ 156 + smoker({ 157 157 error: true, 158 158 text: "invalid url!", 159 159 position: { x: rect.left, y: rect.top - 8 }, ··· 170 170 onMouseDown={(e) => { 171 171 e.preventDefault(); 172 172 if (!linkValue || linkValue === "") { 173 - smoke({ 173 + smoker({ 174 174 error: true, 175 175 text: "no url!", 176 176 position: { x: e.clientX, y: e.clientY }, ··· 178 178 return; 179 179 } 180 180 if (!isUrl(linkValue)) { 181 - smoke({ 181 + smoker({ 182 182 error: true, 183 183 text: "invalid url!", 184 184 position: { x: e.clientX, y: e.clientY },
+10 -7
components/Blocks/ExternalLinkBlock.tsx
··· 42 42 id={props.preview ? undefined : elementId.block(props.entityID).input} 43 43 className={` 44 44 w-full h-[104px] p-2 45 - text-tertiary hover:text-accent-contrast hover:cursor-pointer 46 - flex flex-auto gap-2 items-center justify-center hover:border-2 border-dashed rounded-lg 47 - ${isSelected ? "border-2 border-tertiary" : "border border-border"} 45 + text-tertiary hover:text-accent-contrast hover:cursor-pointer 46 + flex flex-auto gap-2 items-center justify-center hover:border-2 border-dashed rounded-lg 47 + ${isSelected ? "border-2 border-tertiary" : "border border-border"} 48 48 ${props.pageType === "canvas" && "bg-bg-page"}`} 49 49 onMouseDown={() => { 50 50 focusBlock( ··· 153 153 { type: "start" }, 154 154 ); 155 155 }; 156 - let smoke = useSmoker(); 156 + let smoker = useSmoker(); 157 157 158 158 return ( 159 159 <div className={`max-w-sm flex gap-2 rounded-md text-secondary`}> ··· 178 178 if (!linkValue) return; 179 179 if (!isUrl(linkValue)) { 180 180 let rect = e.currentTarget.getBoundingClientRect(); 181 - smoke({ 181 + smoker({ 182 + alignOnMobile: "left", 182 183 error: true, 183 184 text: "invalid url!", 184 185 position: { x: rect.left, y: rect.top - 8 }, ··· 196 197 onMouseDown={(e) => { 197 198 e.preventDefault(); 198 199 if (!linkValue || linkValue === "") { 199 - smoke({ 200 + smoker({ 201 + alignOnMobile: "left", 200 202 error: true, 201 203 text: "no url!", 202 204 position: { x: e.clientX, y: e.clientY }, ··· 204 206 return; 205 207 } 206 208 if (!isUrl(linkValue)) { 207 - smoke({ 209 + smoker({ 210 + alignOnMobile: "left", 208 211 error: true, 209 212 text: "invalid url!", 210 213 position: { x: e.clientX, y: e.clientY },
+262
components/Blocks/RSVPBlock/ContactDetailsForm.tsx
··· 1 + "use client"; 2 + import { useSmoker } from "components/Toast"; 3 + import { RSVP_Status, State, useRSVPNameState } from "."; 4 + import { createContext, useContext, useState } from "react"; 5 + import { useRSVPData } from "src/hooks/useRSVPData"; 6 + import { confirmPhoneAuthToken } from "actions/phone_auth/confirm_phone_auth_token"; 7 + import { submitRSVP } from "actions/phone_rsvp_to_event"; 8 + 9 + import { countryCodes } from "src/constants/countryCodes"; 10 + import { Checkbox } from "components/Checkbox"; 11 + import { ButtonPrimary } from "components/Buttons"; 12 + import { Separator } from "components/Layout"; 13 + import { createPhoneAuthToken } from "actions/phone_auth/request_phone_auth_token"; 14 + import { Input } from "components/Input"; 15 + import { IPLocationContext } from "components/Providers/IPLocationProvider"; 16 + 17 + export function ContactDetailsForm({ 18 + status, 19 + entityID, 20 + }: { 21 + status: RSVP_Status; 22 + entityID: string; 23 + setState: (s: State) => void; 24 + }) { 25 + let focusWithinStyles = 26 + "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 + 29 + let { data, mutate } = useRSVPData(); 30 + let [state, setState] = useState< 31 + { state: "details" } | { state: "confirm"; token: string } 32 + >({ state: "details" }); 33 + let { name, setName } = useRSVPNameState(); 34 + let ipLocation = useContext(IPLocationContext) || "US"; 35 + const [formState, setFormState] = useState({ 36 + country_code: 37 + countryCodes.find((c) => c[1].toUpperCase() === ipLocation)?.[2] || "1", 38 + phone_number: "", 39 + confirmationCode: "", 40 + }); 41 + let [enterNewNumber, setEnterNewNumber] = useState(false); 42 + 43 + let submit = async ( 44 + token: Awaited<ReturnType<typeof confirmPhoneAuthToken>>, 45 + ) => { 46 + try { 47 + await submitRSVP({ 48 + status, 49 + name: name, 50 + entity: entityID, 51 + }); 52 + } catch (e) { 53 + //handle failed confirm 54 + return false; 55 + } 56 + 57 + mutate({ 58 + authToken: token, 59 + rsvps: [ 60 + ...(data?.rsvps || []).filter((r) => r.entity !== entityID), 61 + { 62 + name: name, 63 + status, 64 + entity: entityID, 65 + phone_number: token.phone_number, 66 + country_code: token.country_code, 67 + }, 68 + ], 69 + }); 70 + return true; 71 + }; 72 + return state.state === "details" ? ( 73 + <div className="rsvpForm flex flex-col gap-2"> 74 + <div className="rsvpInputs flex sm:flex-row flex-col gap-2 w-fit place-self-center "> 75 + <label 76 + htmlFor="rsvp-name-input" 77 + className={` 78 + rsvpNameInput input-with-border basis-1/3 h-fit 79 + flex flex-col ${focusWithinStyles}`} 80 + > 81 + <div className="text-xs font-bold italic text-tertiary">name</div> 82 + <Input 83 + autoFocus 84 + id="rsvp-name-input" 85 + placeholder="..." 86 + className=" bg-transparent disabled:text-tertiary w-full appearance-none focus:outline-0" 87 + value={name} 88 + onChange={(e) => setName(e.target.value)} 89 + /> 90 + </label> 91 + <div 92 + className={`rsvpPhoneInputWrapper relative flex flex-col gap-0.5 w-full basis-2/3`} 93 + > 94 + <label 95 + htmlFor="rsvp-phone-input" 96 + className={` 97 + rsvpPhoneInput input-with-border 98 + flex flex-col ${focusWithinStyles} 99 + ${!!data?.authToken?.phone_number && "bg-border-light border-border-light text-tertiary"}`} 100 + > 101 + <div className=" text-xs font-bold italic text-tertiary"> 102 + WhatsApp Number 103 + </div> 104 + <div className="flex gap-2 "> 105 + <div className="flex items-center gap-1"> 106 + <span>+</span> 107 + <Input 108 + onKeyDown={(e) => { 109 + if (e.key === "Backspace" && !e.currentTarget.value) 110 + e.preventDefault(); 111 + }} 112 + disabled={!!data?.authToken?.phone_number} 113 + className="w-10 bg-transparent" 114 + placeholder="1" 115 + maxLength={4} 116 + inputMode="numeric" 117 + pattern="[0-9]*" 118 + value={formState.country_code} 119 + onChange={(e) => 120 + setFormState((s) => ({ 121 + ...s, 122 + country_code: e.target.value.replace(/[^0-9]/g, ""), 123 + })) 124 + } 125 + /> 126 + </div> 127 + <Separator /> 128 + 129 + <Input 130 + id="rsvp-phone-input" 131 + inputMode="numeric" 132 + placeholder="0000000000" 133 + pattern="[0-9]*" 134 + className=" bg-transparent disabled:text-tertiary w-full appearance-none focus:outline-0" 135 + disabled={!!data?.authToken?.phone_number} 136 + onKeyDown={(e) => { 137 + if (e.key === "Backspace" && !e.currentTarget.value) 138 + e.preventDefault(); 139 + }} 140 + value={data?.authToken?.phone_number || formState.phone_number} 141 + onChange={(e) => 142 + setFormState((state) => ({ 143 + ...state, 144 + phone_number: e.target.value.replace(/[^0-9]/g, ""), 145 + })) 146 + } 147 + /> 148 + </div> 149 + </label> 150 + <div className="text-xs italic text-tertiary leading-tight"> 151 + Non-US numbers will receive messages through{" "} 152 + <strong>WhatsApp</strong> 153 + </div> 154 + </div> 155 + </div> 156 + 157 + <hr className="border-border" /> 158 + <div className="flex flex-row gap-2 w-full items-center justify-end"> 159 + <ConsentPopover checked={checked} setChecked={setChecked} /> 160 + <ButtonPrimary 161 + disabled={ 162 + (!data?.authToken?.phone_number && 163 + (!checked || 164 + !formState.phone_number || 165 + !formState.country_code)) || 166 + (!!data?.authToken?.phone_number && !checked) 167 + } 168 + 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 + }} 177 + > 178 + RSVP as {status === "GOING" ? "Going" : "Maybe"} 179 + </ButtonPrimary> 180 + </div> 181 + </div> 182 + ) : ( 183 + <ConfirmationForm 184 + token={state.token} 185 + value={formState.confirmationCode} 186 + submit={submit} 187 + onChange={(value) => 188 + setFormState((state) => ({ ...state, confirmationCode: value })) 189 + } 190 + /> 191 + ); 192 + } 193 + 194 + const ConfirmationForm = (props: { 195 + value: string; 196 + token: string; 197 + submit: ( 198 + token: Awaited<ReturnType<typeof confirmPhoneAuthToken>>, 199 + ) => Promise<boolean>; 200 + onChange: (v: string) => void; 201 + }) => { 202 + let smoker = useSmoker(); 203 + return ( 204 + <div className="flex flex-col gap-2"> 205 + <label className="rsvpNameInput relative w-full flex flex-col gap-0.5"> 206 + <div className="absolute top-0.5 left-[6px] text-xs font-bold italic text-tertiary"> 207 + confirmation code 208 + </div> 209 + <Input 210 + autoFocus 211 + placeholder="000000" 212 + className="input-with-border !pt-5 w-full " 213 + value={props.value} 214 + onChange={(e) => props.onChange(e.target.value)} 215 + /> 216 + <div className="text-xs italic text-tertiary leading-tight"> 217 + we texted a confirmation code to your phone number! 218 + </div> 219 + </label> 220 + <hr className="border-border" /> 221 + 222 + <ButtonPrimary 223 + 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 + }} 238 + > 239 + Confirm 240 + </ButtonPrimary> 241 + </div> 242 + ); 243 + }; 244 + 245 + const ConsentPopover = (props: { 246 + checked: boolean; 247 + setChecked: (checked: boolean) => void; 248 + }) => { 249 + return ( 250 + <Checkbox 251 + checked={props.checked} 252 + onChange={() => { 253 + props.setChecked(!props.checked); 254 + }} 255 + > 256 + <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! 259 + </div> 260 + </Checkbox> 261 + ); 262 + };
+367
components/Blocks/RSVPBlock/index.tsx
··· 1 + "use client"; 2 + import { Database } from "supabase/database.types"; 3 + import { BlockProps } from "components/Blocks/Block"; 4 + import { createContext, useContext, useState } from "react"; 5 + import { submitRSVP } from "actions/phone_rsvp_to_event"; 6 + import { useRSVPData } from "src/hooks/useRSVPData"; 7 + import { useEntitySetContext } from "components/EntitySetProvider"; 8 + import { 9 + ButtonPrimary, 10 + ButtonSecondary, 11 + ButtonTertiary, 12 + } from "components/Buttons"; 13 + import { UpdateSmall } from "components/Icons"; 14 + import { Popover } from "components/Popover"; 15 + import { create } from "zustand"; 16 + import { combine, createJSONStorage, persist } from "zustand/middleware"; 17 + import { useUIState } from "src/useUIState"; 18 + import { Separator } from "components/Layout"; 19 + import { theme } from "tailwind.config"; 20 + import { useToaster } from "components/Toast"; 21 + import { sendUpdateToRSVPS } from "actions/sendUpdateToRSVPS"; 22 + import { useReplicache } from "src/replicache"; 23 + import { ContactDetailsForm } from "./ContactDetailsForm"; 24 + 25 + export type RSVP_Status = Database["public"]["Enums"]["rsvp_status"]; 26 + let Statuses = ["GOING", "NOT_GOING", "MAYBE"]; 27 + export type State = 28 + | { 29 + state: "default"; 30 + } 31 + | { state: "contact_details"; status: RSVP_Status }; 32 + 33 + export function RSVPBlock(props: BlockProps) { 34 + let isSelected = useUIState((s) => 35 + s.selectedBlocks.find((b) => b.value === props.entityID), 36 + ); 37 + return ( 38 + <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"}`} 40 + style={{ 41 + backgroundColor: 42 + "color-mix(in oklab, rgb(var(--accent-contrast)), rgb(var(--bg-page)) 85%)", 43 + }} 44 + > 45 + <RSVPForm entityID={props.entityID} /> 46 + </div> 47 + ); 48 + } 49 + 50 + function RSVPForm(props: { entityID: string }) { 51 + let [state, setState] = useState<State>({ state: "default" }); 52 + let { permissions } = useEntitySetContext(); 53 + let { data, mutate } = useRSVPData(); 54 + let setStatus = (status: RSVP_Status) => { 55 + setState({ status, state: "contact_details" }); 56 + }; 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) 68 + return permissions.write ? ( 69 + //AND YOU'RE A HOST 70 + <> 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" /> 74 + 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 + <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> 87 + ); 88 + 89 + // IF YOU HAVEN'T RSVP'D 90 + if (state.state === "default") 91 + return permissions.write ? ( 92 + //YOU'RE A HOST 93 + <> 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" /> 112 + </> 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 + ); 132 + 133 + // IF YOU ARE CURRENTLY CONFIRMING YOUR CONTACT DETAILS 134 + if (state.state === "contact_details") 135 + return ( 136 + <ContactDetailsForm 137 + status={state.status} 138 + setState={setState} 139 + entityID={props.entityID} 140 + /> 141 + ); 142 + } 143 + 144 + function YourRSVPStatus(props: { entityID: string; compact?: boolean }) { 145 + let { data, mutate } = useRSVPData(); 146 + let { name } = useRSVPNameState(); 147 + 148 + let rsvpStatus = data?.rsvps?.find( 149 + (rsvp) => 150 + data.authToken && 151 + rsvp.entity === props.entityID && 152 + data.authToken.phone_number === rsvp.phone_number, 153 + )?.status; 154 + 155 + let updateStatus = async (status: RSVP_Status) => { 156 + if (!data?.authToken) return; 157 + await submitRSVP({ 158 + status, 159 + name: name, 160 + entity: props.entityID, 161 + }); 162 + 163 + mutate({ 164 + authToken: data.authToken, 165 + rsvps: [ 166 + ...(data?.rsvps || []).filter((r) => r.entity !== props.entityID), 167 + { 168 + name: name, 169 + status, 170 + entity: props.entityID, 171 + phone_number: data.authToken.phone_number, 172 + country_code: data.authToken.country_code, 173 + }, 174 + ], 175 + }); 176 + }; 177 + return ( 178 + <div 179 + className={`flex flex-row gap-1 sm:gap-2 font-bold items-center ${props.compact ? "text-sm sm:font-bold font-normal" : ""}`} 180 + > 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")} 193 + > 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 + )} 214 + </div> 215 + ); 216 + } 217 + 218 + function Attendees(props: { entityID: string; className?: string }) { 219 + let { data, mutate } = useRSVPData(); 220 + let attendees = 221 + data?.rsvps.filter((rsvp) => rsvp.entity === props.entityID) || []; 222 + let going = attendees.filter((rsvp) => rsvp.status === "GOING"); 223 + let maybe = attendees.filter((rsvp) => rsvp.status === "MAYBE"); 224 + let notGoing = attendees.filter((rsvp) => rsvp.status === "NOT_GOING"); 225 + 226 + return ( 227 + <Popover 228 + align="start" 229 + className="text-sm text-secondary flex flex-col gap-2 max-w-sm" 230 + asChild 231 + trigger={ 232 + going.length === 0 && maybe.length === 0 ? ( 233 + <button 234 + className={`w-max text-tertiary italic hover:underline ${props.className}`} 235 + > 236 + No RSVPs yet 237 + </button> 238 + ) : ( 239 + <ButtonTertiary className={props.className}> 240 + {going.length > 0 && `${going.length} Going`} 241 + {maybe.length > 0 && 242 + `${going.length > 0 ? ", " : ""}${maybe.length} Maybe`} 243 + </ButtonTertiary> 244 + ) 245 + } 246 + > 247 + {going.length === 0 && maybe.length === 0 && notGoing.length === 0 && ( 248 + <div className="text-tertiary italic">No RSVPs yet</div> 249 + )} 250 + {going.length > 0 && ( 251 + <div className="flex flex-col gap-0.5"> 252 + <div className="font-bold text-tertiary">Going ({going.length})</div> 253 + {going.map((rsvp) => ( 254 + <div key={rsvp.phone_number}>{rsvp.name}</div> 255 + ))} 256 + </div> 257 + )} 258 + {maybe.length > 0 && ( 259 + <div className="flex flex-col gap-0"> 260 + <div className="font-bold text-tertiary">Maybe ({maybe.length})</div> 261 + {maybe.map((rsvp) => ( 262 + <div key={rsvp.phone_number}>{rsvp.name}</div> 263 + ))} 264 + </div> 265 + )} 266 + {notGoing.length > 0 && ( 267 + <div className="flex flex-col gap-0"> 268 + <div className="font-bold text-tertiary"> 269 + Can&apos;t Go ({notGoing.length}) 270 + </div> 271 + {notGoing.map((rsvp) => ( 272 + <div key={rsvp.phone_number}>{rsvp.name}</div> 273 + ))} 274 + </div> 275 + )} 276 + </Popover> 277 + ); 278 + } 279 + 280 + function SendUpdateButton(props: { entityID: string }) { 281 + let { permissions } = useEntitySetContext(); 282 + let { permission_token } = useReplicache(); 283 + let [input, setInput] = useState(""); 284 + let toaster = useToaster(); 285 + let [open, setOpen] = useState(false); 286 + 287 + if (!!!permissions.write) return; 288 + return ( 289 + <Popover 290 + asChild 291 + open={open} 292 + onOpenChange={(open) => setOpen(open)} 293 + trigger={ 294 + <ButtonPrimary fullWidthOnMobile> 295 + <UpdateSmall /> Send an Update 296 + </ButtonPrimary> 297 + } 298 + > 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> 306 + 307 + <textarea 308 + id="rsvp-message-input" 309 + value={input} 310 + onChange={(e) => { 311 + setInput(e.target.value); 312 + }} 313 + className="input-with-border w-full h-[150px] mt-1 pt-0.5 font-normal text-primary" 314 + /> 315 + </label> 316 + <div className="flex justify-between items-start"> 317 + <div 318 + className={`rsvpMessageCharCounter text-sm text-tertiary`} 319 + style={ 320 + input.length > 300 321 + ? { 322 + color: theme.colors["accent-contrast"], 323 + fontWeight: "bold", 324 + } 325 + : { 326 + color: theme.colors["tertiary"], 327 + } 328 + } 329 + > 330 + {input.length}/300 {input.length > 300 && " (too long!)"} 331 + </div> 332 + <ButtonPrimary 333 + disabled={input.length > 300} 334 + className="place-self-end " 335 + onClick={async () => { 336 + if (!permission_token) return; 337 + await sendUpdateToRSVPS(permission_token, { 338 + entity: props.entityID, 339 + message: input, 340 + eventName: "Idk still figuring this out", 341 + }); 342 + toaster({ 343 + content: <div className="font-bold">Update sent!</div>, 344 + type: "success", 345 + }); 346 + setOpen(false); 347 + }} 348 + > 349 + Send 350 + </ButtonPrimary> 351 + </div> 352 + </div> 353 + </Popover> 354 + ); 355 + } 356 + 357 + export let useRSVPNameState = create( 358 + persist( 359 + combine({ name: "" }, (set) => ({ 360 + setName: (name: string) => set({ name }), 361 + })), 362 + { 363 + name: "rsvp-name", 364 + storage: createJSONStorage(() => localStorage), 365 + }, 366 + ), 367 + );
+1 -1
components/Blocks/useBlockKeyboardHandlers.ts
··· 51 51 el.tagName === "TEXTAREA" || 52 52 el.contentEditable === "true" 53 53 ) { 54 - if ((el as HTMLInputElement).value !== "") return; 54 + if ((el as HTMLInputElement).value !== "" || e.key === "Tab") return; 55 55 } 56 56 57 57 command?.({
+53
components/Buttons.tsx
··· 47 47 }); 48 48 ButtonPrimary.displayName = "ButtonPrimary"; 49 49 50 + export const ButtonSecondary = forwardRef< 51 + HTMLButtonElement, 52 + { 53 + fullWidth?: boolean; 54 + children: React.ReactNode; 55 + compact?: boolean; 56 + } & ButtonProps 57 + >((props, ref) => { 58 + return ( 59 + <button 60 + {...props} 61 + 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 "} 63 + bg-bg-page outline-transparent 64 + rounded-md text-base font-bold text-accent-contrast 65 + flex gap-2 items-center justify-center shrink-0 66 + transparent-outline hover:outline-accent-contrast outline-offset-1 67 + border border-accent-contrast 68 + disabled:bg-border-light disabled:text-border disabled:hover:text-border 69 + ${props.className} 70 + `} 71 + > 72 + {props.children} 73 + </button> 74 + ); 75 + }); 76 + ButtonSecondary.displayName = "ButtonSecondary"; 77 + 78 + export const ButtonTertiary = forwardRef< 79 + HTMLButtonElement, 80 + { 81 + fullWidth?: boolean; 82 + children: React.ReactNode; 83 + compact?: boolean; 84 + } & ButtonProps 85 + >((props, ref) => { 86 + return ( 87 + <button 88 + {...props} 89 + ref={ref} 90 + className={`m-0 h-max ${props.fullWidth ? "w-full" : "w-max"} ${props.compact ? "px-0" : "px-1"} 91 + bg-transparent text-base font-bold text-accent-contrast 92 + flex gap-2 items-center justify-center shrink-0 93 + hover:underline disabled:text-border 94 + ${props.className} 95 + `} 96 + > 97 + {props.children} 98 + </button> 99 + ); 100 + }); 101 + ButtonTertiary.displayName = "ButtonTertiary"; 102 + 50 103 export const HoverButton = (props: { 51 104 id?: string; 52 105 icon: React.ReactNode;
+1 -1
components/Checkbox.tsx
··· 9 9 }) { 10 10 return ( 11 11 <label 12 - className={`flex gap-2 items-start cursor-pointer shrink-0 ${props.checked ? "text-primary font-bold " : " text-tertiary font-normal"}`} 12 + className={`flex w-full gap-2 items-start cursor-pointer ${props.checked ? "text-primary font-bold " : " text-tertiary font-normal"}`} 13 13 > 14 14 <input 15 15 type="checkbox"
+40
components/Icons.tsx
··· 485 485 ); 486 486 }; 487 487 488 + export const RSVPSmall = (props: Props) => { 489 + return ( 490 + <svg 491 + width="24" 492 + height="24" 493 + viewBox="0 0 24 24" 494 + fill="none" 495 + xmlns="http://www.w3.org/2000/svg" 496 + {...props} 497 + > 498 + <path 499 + fillRule="evenodd" 500 + clipRule="evenodd" 501 + d="M8.26788 2.26052C8.76343 1.73099 9.53423 1.56198 10.2059 1.83559L11.7491 2.46429L16.6025 2.20414C17.4868 2.15673 18.2463 2.82619 18.3102 3.7095L18.4144 5.14935L20.0677 6.01329C20.6659 6.32586 21.0921 6.89045 21.229 7.55135L23.019 16.1987C23.2697 17.4099 22.4958 18.5964 21.2863 18.8551L5.44248 22.2436C4.22165 22.5047 3.02167 21.7219 2.76862 20.4994L0.980561 11.8611C0.845583 11.209 1.0064 10.5306 1.41972 10.0085L3.56607 7.29716L3.40145 4.5493C3.34755 3.64946 4.0364 2.8777 4.93657 2.82945L7.88327 2.6715L8.26788 2.26052ZM18.6163 7.93856L18.5417 6.90833L19.373 7.34272C19.5724 7.44691 19.7145 7.63511 19.7601 7.85541L21.5501 16.5028C21.6337 16.9065 21.3757 17.302 20.9725 17.3882L5.12877 20.7767C4.72183 20.8638 4.32183 20.6028 4.23748 20.1953L2.44942 11.557C2.40443 11.3397 2.45804 11.1135 2.59581 10.9395L3.70066 9.54382L3.80512 11.2874L3.64416 11.2345C3.31624 11.1267 2.96303 11.3051 2.85524 11.6331C2.74745 11.961 2.9259 12.3142 3.25381 12.422L12.2571 15.3814L19.2176 8.99821C19.472 8.76491 19.4891 8.36956 19.2558 8.11516C19.0886 7.93292 18.8384 7.87245 18.6163 7.93856ZM17.4353 8.93663L17.0635 3.79974C17.0487 3.5959 16.8735 3.4414 16.6694 3.45234L5.00347 4.07766C4.79574 4.0888 4.63678 4.26689 4.64922 4.47455L5.08251 11.7073L11.9518 13.9653L17.4353 8.93663ZM11.1605 6.7859C11.5132 5.9923 11.7901 5.76891 12.5288 5.72291C13.3194 5.67369 14.0001 6.35526 13.9068 7.44072C13.8015 8.66549 13.1609 9.84543 11.4351 11.1966C9.55455 10.07 8.50235 9.10035 8.24589 7.89811C8.0186 6.83261 8.66576 5.94549 9.45655 5.89625C10.1954 5.85024 10.7119 6.04219 11.1605 6.7859Z" 502 + fill="currentColor" 503 + /> 504 + </svg> 505 + ); 506 + }; 507 + 488 508 export const AccountSmall = (props: Props) => { 489 509 return ( 490 510 <svg ··· 504 524 </svg> 505 525 ); 506 526 }; 527 + 507 528 export const ShareSmall = (props: Props) => { 508 529 return ( 509 530 <svg ··· 1310 1331 fillRule="evenodd" 1311 1332 clipRule="evenodd" 1312 1333 d="M7.89376 6.62482C7.89376 4.35699 9.7322 2.51855 12 2.51855C14.2678 2.51855 16.1063 4.35699 16.1063 6.62482V10.1794H16.2115C17.178 10.1794 17.9615 10.9629 17.9615 11.9294V17C17.9615 18.6569 16.6184 20 14.9615 20H9.03854C7.38168 20 6.03854 18.6569 6.03854 17V11.9294C6.03854 10.9629 6.82204 10.1794 7.78854 10.1794H14.3563V6.62482C14.3563 5.32349 13.3013 4.26855 12 4.26855C10.6987 4.26855 9.64376 5.32349 9.64376 6.62482V7.72078C9.64376 8.20403 9.25201 8.59578 8.76876 8.59578C8.28551 8.59578 7.89376 8.20403 7.89376 7.72078V6.62482ZM13.1496 14.2193C13.1496 14.7123 12.8693 15.1399 12.4593 15.3512L12.6405 17.2714C12.6544 17.4181 12.539 17.5449 12.3916 17.5449H11.3672C11.218 17.5449 11.1021 17.4152 11.1187 17.267L11.3317 15.3696C10.9016 15.1654 10.6043 14.7271 10.6043 14.2193C10.6043 13.5165 11.1741 12.9467 11.8769 12.9467C12.5798 12.9467 13.1496 13.5165 13.1496 14.2193ZM5.62896 5.3862C5.4215 5.20395 5.10558 5.2244 4.92333 5.43186C4.74109 5.63932 4.76153 5.95525 4.969 6.13749L6.06209 7.09771C6.26955 7.27996 6.58548 7.25951 6.76772 7.05205C6.94997 6.84458 6.92952 6.52866 6.72206 6.34642L5.62896 5.3862ZM3.5165 6.64283C3.25418 6.55657 2.97159 6.69929 2.88533 6.96161C2.79906 7.22393 2.94178 7.50652 3.20411 7.59278L5.54822 8.36366C5.81054 8.44992 6.09313 8.3072 6.1794 8.04488C6.26566 7.78256 6.12294 7.49997 5.86062 7.41371L3.5165 6.64283ZM3.54574 9.42431C3.52207 9.14918 3.72592 8.90696 4.00105 8.8833L5.52254 8.75244C5.79766 8.72878 6.03988 8.93263 6.06354 9.20776C6.08721 9.48288 5.88335 9.7251 5.60823 9.74876L4.08674 9.87962C3.81162 9.90329 3.5694 9.69943 3.54574 9.42431Z" 1334 + fill="currentColor" 1335 + /> 1336 + </svg> 1337 + ); 1338 + }; 1339 + 1340 + export const UpdateSmall = (props: Props) => { 1341 + return ( 1342 + <svg 1343 + width="24" 1344 + height="24" 1345 + viewBox="0 0 24 24" 1346 + fill="none" 1347 + xmlns="http://www.w3.org/2000/svg" 1348 + > 1349 + <path 1350 + fillRule="evenodd" 1351 + clipRule="evenodd" 1352 + d="M9.22186 7.92684C10.1774 6.18312 11.5332 4.90336 12.9251 4.2286C13.1335 4.12754 13.3416 4.04046 13.5484 3.96745C14.6049 3.60869 15.7766 3.54735 16.7819 4.09825C17.8692 4.69405 18.5671 5.88122 18.7476 7.41916C18.9279 8.95543 18.5788 10.7869 17.6233 12.5306C16.6678 14.2743 15.312 15.5541 13.9201 16.2288C12.5267 16.9043 11.1506 16.955 10.0633 16.3592C9.19584 15.8839 8.57626 15.0321 8.26951 13.9262C8.25817 13.8746 8.24668 13.8234 8.23523 13.7724L8.23523 13.7724C8.18078 13.5299 8.12744 13.2924 8.09762 13.0383C7.91733 11.502 8.26635 9.67055 9.22186 7.92684ZM9.46946 4.78715C9.67119 4.78662 9.8633 4.78121 10.0481 4.7711C9.3182 5.48646 8.66218 6.34702 8.12565 7.32615C7.55376 8.36979 7.16847 9.45536 6.96726 10.519C6.87184 10.3382 6.77397 10.1659 6.67468 10.0061C6.66248 9.63279 6.756 9.17519 6.92538 8.67954C7.12252 8.10267 7.40257 7.53025 7.65185 7.07532C7.87489 6.6683 8.26315 6.06477 8.68993 5.5499C8.9033 5.29248 9.11698 5.06859 9.31569 4.90418C9.37126 4.8582 9.42255 4.81949 9.46946 4.78715ZM8.11028 4.69028C7.79498 4.62946 7.4876 4.54412 7.23739 4.46669C6.91656 4.36741 6.66099 4.27202 6.54912 4.22896C6.41134 4.17536 6.19445 4.14 6.05859 4.21094C5.71409 4.39084 5.01295 4.92363 4.69271 5.51519C4.53469 5.8071 4.40424 6.2273 4.30596 6.64793C4.29259 6.70518 4.27708 6.76449 4.26123 6.82511L4.26123 6.82512L4.26122 6.82514C4.18998 7.09762 4.11179 7.39666 4.18884 7.65503C4.24062 7.82867 4.31432 7.93693 4.39162 8.00286C4.59287 8.12133 4.78982 8.24738 4.98348 8.37782C5.22591 8.54111 5.52054 8.75196 5.79607 8.98466C5.84667 8.7703 5.90975 8.55912 5.97911 8.35617C6.20171 7.70478 6.51068 7.07692 6.77488 6.59477C7.02425 6.1397 7.44733 5.482 7.92003 4.91174C7.98204 4.83692 8.04556 4.76282 8.11028 4.69028ZM4.21574 3.89626L4.62051 4.02189C4.3203 4.30946 4.01949 4.65825 3.8133 5.03912C3.59059 5.45053 3.43618 5.9753 3.33219 6.42041C3.30438 6.53942 3.27957 6.65546 3.25762 6.7656L2.81215 6.40882C2.81215 6.40882 2.81126 6.40681 2.80986 6.40423C2.79662 6.37992 2.73103 6.25944 2.74152 5.96321C2.75269 5.6481 2.85108 5.26172 3.04578 4.90642C3.25394 4.52653 3.50079 4.23769 3.73458 4.06623C3.95711 3.90302 4.11635 3.8793 4.21574 3.89626ZM5.25013 10.1776C5.49632 10.4247 5.83445 10.991 6.17145 11.7406C5.73841 12.4265 5.41616 12.6857 5.21838 12.7691C5.07131 12.8312 4.93508 12.822 4.70214 12.656C4.11675 12.2388 3.60414 11.8264 3.21764 11.4066C2.8298 10.9853 2.60401 10.594 2.53069 10.2224L2.52687 10.2031C2.4802 9.9669 2.45604 9.84466 2.51608 9.58542C2.57686 9.32295 2.72752 8.9236 3.07623 8.2506C3.19924 8.54228 3.38803 8.81394 3.66359 9.02041C3.77639 9.10493 3.89934 9.17816 4.02211 9.25128L4.02211 9.25128C4.11121 9.30434 4.20021 9.35735 4.28517 9.41458C4.61144 9.63434 4.98505 9.91153 5.25013 10.1776ZM1.49231 5.91896C1.47179 6.49822 1.63299 7.06591 2.09331 7.43458C1.64229 8.27701 1.40278 8.85224 1.2983 9.30341C1.17766 9.82436 1.24402 10.1596 1.29968 10.4408L1.30433 10.4643C1.43907 11.1472 1.82601 11.7405 2.29799 12.2532C2.77132 12.7673 3.36564 13.2385 3.9767 13.6739C4.42074 13.9904 5.0195 14.2097 5.70419 13.9209C6.06177 13.77 6.39891 13.496 6.72728 13.1045C6.81994 13.3603 6.90026 13.6093 6.96835 13.8644C7.28444 15.3377 8.1138 16.7163 9.46258 17.4554C10.998 18.2968 12.8155 18.1535 14.4654 17.3536C16.1168 16.5531 17.6539 15.0761 18.7195 13.1313C19.7852 11.1865 20.203 9.09618 19.9891 7.27346C19.7753 5.4524 18.918 3.84341 17.3826 3.00204C16.1201 2.31022 14.6669 2.28413 13.2729 2.74137C13.2652 2.74368 13.2574 2.74615 13.2497 2.74878C11.4939 3.34572 10.626 3.60952 8.78711 3.52059C8.44675 3.50414 7.99961 3.39408 7.60693 3.27256C7.49582 3.23818 7.38646 3.19733 7.27712 3.15649C7.15008 3.10903 7.02308 3.06159 6.89344 3.02433C6.45975 2.89969 6.03009 2.91392 5.62971 3.0263C5.50956 2.98901 5.3892 2.94865 5.26851 2.90817C5.01835 2.82428 4.76678 2.73992 4.51267 2.68142C3.94356 2.55041 3.41069 2.75363 2.99533 3.05825C2.57846 3.36398 2.22138 3.8097 1.94957 4.30573C1.66428 4.82635 1.5106 5.40259 1.49231 5.91896ZM10.6051 8.68425C10.9866 7.98795 11.4394 7.38085 11.9278 6.8783C12.6769 7.53018 13.1717 8.17432 13.4238 8.75106C13.6893 9.35867 13.6744 9.85621 13.4617 10.2444C13.2546 10.6223 12.8385 10.9029 12.1709 11.0084C11.5426 11.1076 10.7313 11.0418 9.794 10.7741C9.95466 10.091 10.2229 9.3816 10.6051 8.68425ZM13.4264 5.71995C13.1758 5.85571 12.9254 6.0188 12.6791 6.20754C13.4537 6.89902 14.0241 7.62766 14.3401 8.35057C14.6935 9.15932 14.7389 9.99457 14.3386 10.7249C13.9392 11.4539 13.1982 11.8584 12.327 11.9961C11.5454 12.1196 10.6234 12.0373 9.63348 11.7675C9.60713 12.0758 9.60447 12.3739 9.62485 12.6574C9.70968 13.8381 10.1817 14.6978 10.9166 15.1005C11.6516 15.5033 12.6302 15.4385 13.671 14.8746C14.7064 14.3136 15.7384 13.2861 16.4923 11.9103C16.776 11.3925 16.9977 10.8667 17.159 10.3487C17.2411 10.0851 17.5214 9.93788 17.785 10.02C18.0487 10.1021 18.1959 10.3824 18.1138 10.646C17.9324 11.2285 17.6845 11.8156 17.3693 12.3909C16.5368 13.91 15.3756 15.0884 14.1473 15.7539C12.9245 16.4164 11.569 16.5983 10.4361 15.9775C9.30313 15.3567 8.72709 14.1163 8.62742 12.7291C8.52731 11.3358 8.89565 9.72284 9.72809 8.2037C10.5605 6.68456 11.7218 5.50611 12.95 4.84069C14.1729 4.17819 15.5283 3.99622 16.6613 4.61705C17.5803 5.12063 18.1356 6.03691 18.3631 7.10207C18.4208 7.37213 18.2486 7.6378 17.9785 7.69548C17.7085 7.75315 17.4428 7.58098 17.3851 7.31093C17.201 6.44889 16.7798 5.82228 16.1807 5.49401C15.4458 5.09129 14.4672 5.15607 13.4264 5.71995ZM20.2049 14.5155C19.8187 14.3656 19.3842 14.5572 19.2343 14.9434C19.0845 15.3295 19.2761 15.764 19.6622 15.9139L21.4114 16.5926C21.7976 16.7425 22.2321 16.5509 22.382 16.1648C22.5318 15.7786 22.3402 15.3441 21.9541 15.1942L20.2049 14.5155ZM17.9326 16.6232C18.2114 16.3169 18.6857 16.2945 18.9921 16.5733L22.8336 20.0686C23.1399 20.3474 23.1623 20.8218 22.8836 21.1281C22.6048 21.4345 22.1304 21.4569 21.8241 21.1781L17.9826 17.6827C17.6762 17.404 17.6539 16.9296 17.9326 16.6232ZM16.8269 17.9194C16.6484 17.5456 16.2007 17.3874 15.8269 17.5659C15.4531 17.7444 15.2949 18.1921 15.4734 18.5659L16.8786 21.5078C17.0572 21.8816 17.5049 22.0398 17.8787 21.8613C18.2524 21.6828 18.4107 21.235 18.2322 20.8613L16.8269 17.9194Z" 1313 1353 fill="currentColor" 1314 1354 /> 1315 1355 </svg>
+14
components/Providers/IPLocationProvider.tsx
··· 1 + "use client"; 2 + import { createContext } from "react"; 3 + 4 + export const IPLocationContext = createContext<string | null>(null); 5 + export const IPLocationProvider = (props: { 6 + country: string | null; 7 + children: React.ReactNode; 8 + }) => { 9 + return ( 10 + <IPLocationContext.Provider value={props.country}> 11 + {props.children} 12 + </IPLocationContext.Provider> 13 + ); 14 + };
+14
components/RSVPDataProvider.tsx
··· 1 + "use client"; 2 + import { getRSVPData } from "actions/getRSVPData"; 3 + import { SWRConfig } from "swr"; 4 + 5 + export function RSVPDataProvider(props: { 6 + data: Awaited<ReturnType<typeof getRSVPData>>; 7 + children: React.ReactNode; 8 + }) { 9 + return ( 10 + <SWRConfig value={{ fallback: { identity: props.data } }}> 11 + {props.children} 12 + </SWRConfig> 13 + ); 14 + }
+1 -1
components/ShareOptions/index.tsx
··· 111 111 ); 112 112 smoker({ 113 113 position: { 114 - x: rect ? rect.left + 80 : 0, 114 + x: rect ? rect.left + (rect.right - rect.left) / 2 : 0, 115 115 y: rect ? rect.top + 26 : 0, 116 116 }, 117 117 text: props.smokerText,
+15 -5
components/Toast.tsx
··· 20 20 text: React.ReactNode; 21 21 static?: boolean; 22 22 error?: boolean; 23 + alignOnMobile?: "left" | "right" | "center" | undefined; 23 24 }; 24 25 25 26 type Smokes = Array<Smoke & { key: string }>; ··· 76 77 error={smoke.error} 77 78 key={smoke.key} 78 79 static={smoke.static} 80 + alignOnMobile={smoke.alignOnMobile} 79 81 > 80 82 {smoke.text} 81 83 </Smoke> ··· 136 138 y: number; 137 139 error?: boolean; 138 140 static?: boolean; 141 + alignOnMobile?: "left" | "right" | "center" | undefined; 139 142 }> 140 143 > = (props) => { 141 144 return ( 142 145 <div 143 - className={`smoke text-center pointer-events-none absolute z-50 rounded-full px-2 py-1 text-sm ${ 144 - props.error 145 - ? "border-white bg-[#dc143c] text-white border font-bold" 146 - : "bg-accent-1 text-accent-2" 147 - }`} 146 + className={`smoke w-max text-center pointer-events-none absolute z-50 rounded-full px-2 py-1 text-sm sm:-translate-x-1/2 ${ 147 + props.alignOnMobile === "left" 148 + ? "-translate-x-full" 149 + : props.alignOnMobile === "right" 150 + ? "" 151 + : "-translate-x-1/2" 152 + } 153 + ${ 154 + props.error 155 + ? "border-white bg-[#dc143c] text-white border font-bold" 156 + : "bg-accent-1 text-accent-2" 157 + }`} 148 158 > 149 159 <style jsx>{` 150 160 .smoke {
+23 -15
drizzle/relations.ts
··· 1 1 import { relations } from "drizzle-orm/relations"; 2 - import { entity_sets, entities, facts, permission_tokens, identities, email_subscriptions_to_entity, email_auth_tokens, permission_token_on_homepage, permission_token_rights } from "./schema"; 2 + import { entities, facts, entity_sets, permission_tokens, identities, email_subscriptions_to_entity, email_auth_tokens, phone_rsvps_to_entity, permission_token_on_homepage, permission_token_rights } from "./schema"; 3 + 4 + export const factsRelations = relations(facts, ({one}) => ({ 5 + entity: one(entities, { 6 + fields: [facts.entity], 7 + references: [entities.id] 8 + }), 9 + })); 3 10 4 11 export const entitiesRelations = relations(entities, ({one, many}) => ({ 12 + facts: many(facts), 5 13 entity_set: one(entity_sets, { 6 14 fields: [entities.set], 7 15 references: [entity_sets.id] 8 16 }), 9 - facts: many(facts), 10 17 permission_tokens: many(permission_tokens), 11 18 email_subscriptions_to_entities: many(email_subscriptions_to_entity), 19 + phone_rsvps_to_entities: many(phone_rsvps_to_entity), 12 20 })); 13 21 14 22 export const entity_setsRelations = relations(entity_sets, ({many}) => ({ ··· 16 24 permission_token_rights: many(permission_token_rights), 17 25 })); 18 26 19 - export const factsRelations = relations(facts, ({one}) => ({ 27 + export const permission_tokensRelations = relations(permission_tokens, ({one, many}) => ({ 20 28 entity: one(entities, { 21 - fields: [facts.entity], 29 + fields: [permission_tokens.root_entity], 22 30 references: [entities.id] 23 31 }), 32 + identities: many(identities), 33 + email_subscriptions_to_entities: many(email_subscriptions_to_entity), 34 + permission_token_on_homepages: many(permission_token_on_homepage), 35 + permission_token_rights: many(permission_token_rights), 24 36 })); 25 37 26 38 export const identitiesRelations = relations(identities, ({one, many}) => ({ ··· 32 44 permission_token_on_homepages: many(permission_token_on_homepage), 33 45 })); 34 46 35 - export const permission_tokensRelations = relations(permission_tokens, ({one, many}) => ({ 36 - identities: many(identities), 37 - entity: one(entities, { 38 - fields: [permission_tokens.root_entity], 39 - references: [entities.id] 40 - }), 41 - email_subscriptions_to_entities: many(email_subscriptions_to_entity), 42 - permission_token_on_homepages: many(permission_token_on_homepage), 43 - permission_token_rights: many(permission_token_rights), 44 - })); 45 - 46 47 export const email_subscriptions_to_entityRelations = relations(email_subscriptions_to_entity, ({one}) => ({ 47 48 entity: one(entities, { 48 49 fields: [email_subscriptions_to_entity.entity], ··· 58 59 identity: one(identities, { 59 60 fields: [email_auth_tokens.identity], 60 61 references: [identities.id] 62 + }), 63 + })); 64 + 65 + export const phone_rsvps_to_entityRelations = relations(phone_rsvps_to_entity, ({one}) => ({ 66 + entity: one(entities, { 67 + fields: [phone_rsvps_to_entity.entity], 68 + references: [entities.id] 61 69 }), 62 70 })); 63 71
+37 -12
drizzle/schema.ts
··· 1 - import { pgTable, foreignKey, pgEnum, uuid, timestamp, text, jsonb, bigint, boolean, primaryKey } from "drizzle-orm/pg-core" 1 + import { pgTable, foreignKey, pgEnum, uuid, text, jsonb, timestamp, bigint, boolean, uniqueIndex, primaryKey } from "drizzle-orm/pg-core" 2 2 import { sql } from "drizzle-orm" 3 3 4 4 export const aal_level = pgEnum("aal_level", ['aal1', 'aal2', 'aal3']) ··· 9 9 export const request_status = pgEnum("request_status", ['PENDING', 'SUCCESS', 'ERROR']) 10 10 export const key_status = pgEnum("key_status", ['default', 'valid', 'invalid', 'expired']) 11 11 export const key_type = pgEnum("key_type", ['aead-ietf', 'aead-det', 'hmacsha512', 'hmacsha256', 'auth', 'shorthash', 'generichash', 'kdf', 'secretbox', 'secretstream', 'stream_xchacha20']) 12 + export const rsvp_status = pgEnum("rsvp_status", ['GOING', 'NOT_GOING', 'MAYBE']) 12 13 export const action = pgEnum("action", ['INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', 'ERROR']) 13 14 export const equality_op = pgEnum("equality_op", ['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'in']) 14 15 15 16 16 - export const entities = pgTable("entities", { 17 - id: uuid("id").primaryKey().notNull(), 18 - created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 19 - set: uuid("set").notNull().references(() => entity_sets.id, { onDelete: "cascade", onUpdate: "cascade" } ), 20 - }); 21 - 22 17 export const facts = pgTable("facts", { 23 18 id: uuid("id").primaryKey().notNull(), 24 19 entity: uuid("entity").notNull().references(() => entities.id, { onDelete: "cascade", onUpdate: "restrict" } ), ··· 37 32 last_mutation: bigint("last_mutation", { mode: "number" }).notNull(), 38 33 }); 39 34 35 + export const entities = pgTable("entities", { 36 + id: uuid("id").primaryKey().notNull(), 37 + created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 38 + set: uuid("set").notNull().references(() => entity_sets.id, { onDelete: "cascade", onUpdate: "cascade" } ), 39 + }); 40 + 40 41 export const entity_sets = pgTable("entity_sets", { 41 42 id: uuid("id").defaultRandom().primaryKey().notNull(), 42 43 created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 43 44 }); 44 45 46 + export const permission_tokens = pgTable("permission_tokens", { 47 + id: uuid("id").defaultRandom().primaryKey().notNull(), 48 + root_entity: uuid("root_entity").notNull().references(() => entities.id, { onDelete: "cascade", onUpdate: "cascade" } ), 49 + }); 50 + 45 51 export const identities = pgTable("identities", { 46 52 id: uuid("id").defaultRandom().primaryKey().notNull(), 47 53 created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 48 54 home_page: uuid("home_page").notNull().references(() => permission_tokens.id, { onDelete: "cascade" } ), 49 55 email: text("email"), 50 - }); 51 - 52 - export const permission_tokens = pgTable("permission_tokens", { 53 - id: uuid("id").defaultRandom().primaryKey().notNull(), 54 - root_entity: uuid("root_entity").notNull().references(() => entities.id, { onDelete: "cascade", onUpdate: "cascade" } ), 55 56 }); 56 57 57 58 export const email_subscriptions_to_entity = pgTable("email_subscriptions_to_entity", { ··· 71 72 email: text("email").notNull(), 72 73 confirmation_code: text("confirmation_code").notNull(), 73 74 identity: uuid("identity").references(() => identities.id, { onDelete: "cascade", onUpdate: "cascade" } ), 75 + }); 76 + 77 + export const phone_number_auth_tokens = pgTable("phone_number_auth_tokens", { 78 + id: uuid("id").defaultRandom().primaryKey().notNull(), 79 + created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 80 + confirmed: boolean("confirmed").default(false).notNull(), 81 + confirmation_code: text("confirmation_code").notNull(), 82 + phone_number: text("phone_number").notNull(), 83 + country_code: text("country_code").notNull(), 84 + }); 85 + 86 + export const phone_rsvps_to_entity = pgTable("phone_rsvps_to_entity", { 87 + created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 88 + phone_number: text("phone_number").notNull(), 89 + status: rsvp_status("status").notNull(), 90 + id: uuid("id").defaultRandom().primaryKey().notNull(), 91 + entity: uuid("entity").notNull().references(() => entities.id, { onDelete: "cascade", onUpdate: "cascade" } ), 92 + name: text("name").default('').notNull(), 93 + country_code: text("country_code").notNull(), 94 + }, 95 + (table) => { 96 + return { 97 + unique_phone_number_entities: uniqueIndex("unique_phone_number_entities").on(table.phone_number, table.entity), 98 + } 74 99 }); 75 100 76 101 export const permission_token_on_homepage = pgTable("permission_token_on_homepage", {
+204 -15
package-lock.json
··· 53 53 "replicache-react": "^5.0.1", 54 54 "swr": "^2.2.5", 55 55 "thumbhash": "^0.1.1", 56 + "twilio": "^5.3.7", 56 57 "unified": "^11.0.5", 57 58 "unist-util-visit": "^5.0.0", 58 59 "uuid": "^10.0.0", ··· 6816 6817 } 6817 6818 }, 6818 6819 "node_modules/axios": { 6819 - "version": "1.7.2", 6820 - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", 6821 - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", 6820 + "version": "1.7.9", 6821 + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", 6822 + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", 6823 + "license": "MIT", 6822 6824 "dependencies": { 6823 6825 "follow-redirects": "^1.15.6", 6824 6826 "form-data": "^4.0.0", ··· 6955 6957 "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" 6956 6958 } 6957 6959 }, 6960 + "node_modules/buffer-equal-constant-time": { 6961 + "version": "1.0.1", 6962 + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 6963 + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", 6964 + "license": "BSD-3-Clause" 6965 + }, 6958 6966 "node_modules/buffer-from": { 6959 6967 "version": "1.1.2", 6960 6968 "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", ··· 6976 6984 "version": "1.0.7", 6977 6985 "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", 6978 6986 "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", 6979 - "dev": true, 6980 6987 "dependencies": { 6981 6988 "es-define-property": "^1.0.0", 6982 6989 "es-errors": "^1.3.0", ··· 7397 7404 "url": "https://github.com/sponsors/kossnocorp" 7398 7405 } 7399 7406 }, 7407 + "node_modules/dayjs": { 7408 + "version": "1.11.13", 7409 + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", 7410 + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", 7411 + "license": "MIT" 7412 + }, 7400 7413 "node_modules/debounce": { 7401 7414 "version": "1.2.1", 7402 7415 "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", ··· 7440 7453 "version": "1.1.4", 7441 7454 "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", 7442 7455 "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", 7443 - "dev": true, 7444 7456 "dependencies": { 7445 7457 "es-define-property": "^1.0.0", 7446 7458 "es-errors": "^1.3.0", ··· 7698 7710 "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", 7699 7711 "dev": true 7700 7712 }, 7713 + "node_modules/ecdsa-sig-formatter": { 7714 + "version": "1.0.11", 7715 + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", 7716 + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", 7717 + "license": "Apache-2.0", 7718 + "dependencies": { 7719 + "safe-buffer": "^5.0.1" 7720 + } 7721 + }, 7701 7722 "node_modules/electron-to-chromium": { 7702 7723 "version": "1.4.783", 7703 7724 "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.783.tgz", ··· 7810 7831 "version": "1.0.0", 7811 7832 "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", 7812 7833 "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", 7813 - "dev": true, 7814 7834 "dependencies": { 7815 7835 "get-intrinsic": "^1.2.4" 7816 7836 }, ··· 7822 7842 "version": "1.3.0", 7823 7843 "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 7824 7844 "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 7825 - "dev": true, 7826 7845 "engines": { 7827 7846 "node": ">= 0.4" 7828 7847 } ··· 8923 8942 "version": "1.2.4", 8924 8943 "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", 8925 8944 "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", 8926 - "dev": true, 8927 8945 "dependencies": { 8928 8946 "es-errors": "^1.3.0", 8929 8947 "function-bind": "^1.1.2", ··· 9105 9123 "version": "1.0.1", 9106 9124 "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", 9107 9125 "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", 9108 - "dev": true, 9109 9126 "dependencies": { 9110 9127 "get-intrinsic": "^1.1.3" 9111 9128 }, ··· 9156 9173 "version": "1.0.2", 9157 9174 "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", 9158 9175 "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", 9159 - "dev": true, 9160 9176 "dependencies": { 9161 9177 "es-define-property": "^1.0.0" 9162 9178 }, ··· 9168 9184 "version": "1.0.3", 9169 9185 "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", 9170 9186 "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", 9171 - "dev": true, 9172 9187 "engines": { 9173 9188 "node": ">= 0.4" 9174 9189 }, ··· 9180 9195 "version": "1.0.3", 9181 9196 "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 9182 9197 "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 9183 - "dev": true, 9184 9198 "engines": { 9185 9199 "node": ">= 0.4" 9186 9200 }, ··· 10212 10226 "json5": "lib/cli.js" 10213 10227 } 10214 10228 }, 10229 + "node_modules/jsonwebtoken": { 10230 + "version": "9.0.2", 10231 + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", 10232 + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", 10233 + "license": "MIT", 10234 + "dependencies": { 10235 + "jws": "^3.2.2", 10236 + "lodash.includes": "^4.3.0", 10237 + "lodash.isboolean": "^3.0.3", 10238 + "lodash.isinteger": "^4.0.4", 10239 + "lodash.isnumber": "^3.0.3", 10240 + "lodash.isplainobject": "^4.0.6", 10241 + "lodash.isstring": "^4.0.1", 10242 + "lodash.once": "^4.0.0", 10243 + "ms": "^2.1.1", 10244 + "semver": "^7.5.4" 10245 + }, 10246 + "engines": { 10247 + "node": ">=12", 10248 + "npm": ">=6" 10249 + } 10250 + }, 10215 10251 "node_modules/jsx-ast-utils": { 10216 10252 "version": "3.3.5", 10217 10253 "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", ··· 10225 10261 }, 10226 10262 "engines": { 10227 10263 "node": ">=4.0" 10264 + } 10265 + }, 10266 + "node_modules/jwa": { 10267 + "version": "1.4.1", 10268 + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", 10269 + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", 10270 + "license": "MIT", 10271 + "dependencies": { 10272 + "buffer-equal-constant-time": "1.0.1", 10273 + "ecdsa-sig-formatter": "1.0.11", 10274 + "safe-buffer": "^5.0.1" 10275 + } 10276 + }, 10277 + "node_modules/jws": { 10278 + "version": "3.2.2", 10279 + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", 10280 + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", 10281 + "license": "MIT", 10282 + "dependencies": { 10283 + "jwa": "^1.4.1", 10284 + "safe-buffer": "^5.0.1" 10228 10285 } 10229 10286 }, 10230 10287 "node_modules/keyv": { ··· 10322 10379 "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", 10323 10380 "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" 10324 10381 }, 10382 + "node_modules/lodash.includes": { 10383 + "version": "4.3.0", 10384 + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", 10385 + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", 10386 + "license": "MIT" 10387 + }, 10388 + "node_modules/lodash.isboolean": { 10389 + "version": "3.0.3", 10390 + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", 10391 + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", 10392 + "license": "MIT" 10393 + }, 10394 + "node_modules/lodash.isinteger": { 10395 + "version": "4.0.4", 10396 + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", 10397 + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", 10398 + "license": "MIT" 10399 + }, 10400 + "node_modules/lodash.isnumber": { 10401 + "version": "3.0.3", 10402 + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", 10403 + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", 10404 + "license": "MIT" 10405 + }, 10406 + "node_modules/lodash.isplainobject": { 10407 + "version": "4.0.6", 10408 + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", 10409 + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", 10410 + "license": "MIT" 10411 + }, 10412 + "node_modules/lodash.isstring": { 10413 + "version": "4.0.1", 10414 + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", 10415 + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", 10416 + "license": "MIT" 10417 + }, 10325 10418 "node_modules/lodash.merge": { 10326 10419 "version": "4.6.2", 10327 10420 "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 10328 10421 "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" 10422 + }, 10423 + "node_modules/lodash.once": { 10424 + "version": "4.1.1", 10425 + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", 10426 + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", 10427 + "license": "MIT" 10329 10428 }, 10330 10429 "node_modules/lodash.throttle": { 10331 10430 "version": "4.1.1", ··· 11882 11981 "version": "1.13.1", 11883 11982 "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", 11884 11983 "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", 11885 - "dev": true, 11886 11984 "funding": { 11887 11985 "url": "https://github.com/sponsors/ljharb" 11888 11986 } ··· 12533 12631 "dev": true, 12534 12632 "engines": { 12535 12633 "node": ">=6" 12634 + } 12635 + }, 12636 + "node_modules/qs": { 12637 + "version": "6.13.1", 12638 + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz", 12639 + "integrity": "sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==", 12640 + "license": "BSD-3-Clause", 12641 + "dependencies": { 12642 + "side-channel": "^1.0.6" 12643 + }, 12644 + "engines": { 12645 + "node": ">=0.6" 12646 + }, 12647 + "funding": { 12648 + "url": "https://github.com/sponsors/ljharb" 12536 12649 } 12537 12650 }, 12538 12651 "node_modules/queue-microtask": { ··· 13297 13410 "url": "https://github.com/sponsors/ljharb" 13298 13411 } 13299 13412 }, 13413 + "node_modules/safe-buffer": { 13414 + "version": "5.2.1", 13415 + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 13416 + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 13417 + "funding": [ 13418 + { 13419 + "type": "github", 13420 + "url": "https://github.com/sponsors/feross" 13421 + }, 13422 + { 13423 + "type": "patreon", 13424 + "url": "https://www.patreon.com/feross" 13425 + }, 13426 + { 13427 + "type": "consulting", 13428 + "url": "https://feross.org/support" 13429 + } 13430 + ], 13431 + "license": "MIT" 13432 + }, 13300 13433 "node_modules/safe-regex-test": { 13301 13434 "version": "1.0.3", 13302 13435 "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", ··· 13321 13454 "dependencies": { 13322 13455 "loose-envify": "^1.1.0" 13323 13456 } 13457 + }, 13458 + "node_modules/scmp": { 13459 + "version": "2.1.0", 13460 + "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz", 13461 + "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==", 13462 + "license": "BSD-3-Clause" 13324 13463 }, 13325 13464 "node_modules/selfsigned": { 13326 13465 "version": "2.4.1", ··· 13355 13494 "version": "1.2.2", 13356 13495 "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", 13357 13496 "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", 13358 - "dev": true, 13359 13497 "dependencies": { 13360 13498 "define-data-property": "^1.1.4", 13361 13499 "es-errors": "^1.3.0", ··· 13413 13551 "version": "1.0.6", 13414 13552 "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", 13415 13553 "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", 13416 - "dev": true, 13417 13554 "dependencies": { 13418 13555 "call-bind": "^1.0.7", 13419 13556 "es-errors": "^1.3.0", ··· 14037 14174 "version": "2.6.2", 14038 14175 "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", 14039 14176 "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" 14177 + }, 14178 + "node_modules/twilio": { 14179 + "version": "5.3.7", 14180 + "resolved": "https://registry.npmjs.org/twilio/-/twilio-5.3.7.tgz", 14181 + "integrity": "sha512-9PaIXQ2CSfKKKjsQ7YpLRi7vo77ryevjoylR+7uSB0D/9t5VOYz+QNxNuR3YGk9UCt/f5pxzjVQraWVI5lQFSQ==", 14182 + "license": "MIT", 14183 + "dependencies": { 14184 + "axios": "^1.7.4", 14185 + "dayjs": "^1.11.9", 14186 + "https-proxy-agent": "^5.0.0", 14187 + "jsonwebtoken": "^9.0.2", 14188 + "qs": "^6.9.4", 14189 + "scmp": "^2.1.0", 14190 + "xmlbuilder": "^13.0.2" 14191 + }, 14192 + "engines": { 14193 + "node": ">=14.0" 14194 + } 14195 + }, 14196 + "node_modules/twilio/node_modules/agent-base": { 14197 + "version": "6.0.2", 14198 + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", 14199 + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", 14200 + "license": "MIT", 14201 + "dependencies": { 14202 + "debug": "4" 14203 + }, 14204 + "engines": { 14205 + "node": ">= 6.0.0" 14206 + } 14207 + }, 14208 + "node_modules/twilio/node_modules/https-proxy-agent": { 14209 + "version": "5.0.1", 14210 + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", 14211 + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", 14212 + "license": "MIT", 14213 + "dependencies": { 14214 + "agent-base": "6", 14215 + "debug": "4" 14216 + }, 14217 + "engines": { 14218 + "node": ">= 6" 14219 + } 14040 14220 }, 14041 14221 "node_modules/type": { 14042 14222 "version": "2.7.2", ··· 15100 15280 "utf-8-validate": { 15101 15281 "optional": true 15102 15282 } 15283 + } 15284 + }, 15285 + "node_modules/xmlbuilder": { 15286 + "version": "13.0.2", 15287 + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz", 15288 + "integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==", 15289 + "license": "MIT", 15290 + "engines": { 15291 + "node": ">=6.0" 15103 15292 } 15104 15293 }, 15105 15294 "node_modules/xxhash-wasm": {
+1
package.json
··· 55 55 "replicache-react": "^5.0.1", 56 56 "swr": "^2.2.5", 57 57 "thumbhash": "^0.1.1", 58 + "twilio": "^5.3.7", 58 59 "unified": "^11.0.5", 59 60 "unist-util-visit": "^5.0.0", 60 61 "uuid": "^10.0.0",
+324
src/constants/countryCodes.ts
··· 1 + export const countryCodes: CountryData[] = [ 2 + ["Afghanistan", "af", "93"], 3 + ["Albania", "al", "355"], 4 + ["Algeria", "dz", "213"], 5 + ["Andorra", "ad", "376"], 6 + ["Angola", "ao", "244"], 7 + ["Antigua and Barbuda", "ag", "1268"], 8 + ["Argentina", "ar", "54", "(..) ........", 0], 9 + ["Armenia", "am", "374", ".. ......"], 10 + ["Aruba", "aw", "297"], 11 + [ 12 + "Australia", 13 + "au", 14 + "61", 15 + { 16 + default: ". .... ....", 17 + "/^4/": "... ... ...", 18 + "/^5(?!50)/": "... ... ...", 19 + "/^1(3|8)00/": ".... ... ...", 20 + "/^13/": ".. .. ..", 21 + "/^180/": "... ....", 22 + }, 23 + 0, 24 + [], 25 + ], 26 + ["Austria", "at", "43"], 27 + ["Azerbaijan", "az", "994", "(..) ... .. .."], 28 + ["Bahamas", "bs", "1242"], 29 + ["Bahrain", "bh", "973"], 30 + ["Bangladesh", "bd", "880"], 31 + ["Barbados", "bb", "1246"], 32 + ["Belarus", "by", "375", "(..) ... .. .."], 33 + ["Belgium", "be", "32", "... .. .. .."], 34 + ["Belize", "bz", "501"], 35 + ["Benin", "bj", "229"], 36 + ["Bhutan", "bt", "975"], 37 + ["Bolivia", "bo", "591"], 38 + ["Bosnia and Herzegovina", "ba", "387"], 39 + ["Botswana", "bw", "267"], 40 + ["Brazil", "br", "55", "(..) ........."], 41 + ["British Indian Ocean Territory", "io", "246"], 42 + ["Brunei", "bn", "673"], 43 + ["Bulgaria", "bg", "359"], 44 + ["Burkina Faso", "bf", "226"], 45 + ["Burundi", "bi", "257"], 46 + ["Cambodia", "kh", "855"], 47 + ["Cameroon", "cm", "237"], 48 + [ 49 + "Canada", 50 + "ca", 51 + "1", 52 + "(...) ...-....", 53 + 1, 54 + [ 55 + "204", 56 + "226", 57 + "236", 58 + "249", 59 + "250", 60 + "289", 61 + "306", 62 + "343", 63 + "365", 64 + "387", 65 + "403", 66 + "416", 67 + "418", 68 + "431", 69 + "437", 70 + "438", 71 + "450", 72 + "506", 73 + "514", 74 + "519", 75 + "548", 76 + "579", 77 + "581", 78 + "587", 79 + "604", 80 + "613", 81 + "639", 82 + "647", 83 + "672", 84 + "705", 85 + "709", 86 + "742", 87 + "778", 88 + "780", 89 + "782", 90 + "807", 91 + "819", 92 + "825", 93 + "867", 94 + "873", 95 + "902", 96 + "905", 97 + ], 98 + ], 99 + ["Cape Verde", "cv", "238"], 100 + ["Caribbean Netherlands", "bq", "599", "", 1], 101 + ["Cayman Islands", "ky", "1", "... ... ....", 4, ["345"]], 102 + ["Central African Republic", "cf", "236"], 103 + ["Chad", "td", "235"], 104 + ["Chile", "cl", "56"], 105 + ["China", "cn", "86", "... .... ...."], 106 + ["Colombia", "co", "57", "... ... ...."], 107 + ["Comoros", "km", "269"], 108 + ["Congo", "cd", "243"], 109 + ["Congo", "cg", "242"], 110 + ["Costa Rica", "cr", "506", "....-...."], 111 + ["Côte d'Ivoire", "ci", "225", ".. .. .. .. .."], 112 + ["Croatia", "hr", "385"], 113 + ["Cuba", "cu", "53"], 114 + ["Curaçao", "cw", "599", "", 0], 115 + ["Cyprus", "cy", "357", ".. ......"], 116 + ["Czech Republic", "cz", "420", "... ... ..."], 117 + ["Denmark", "dk", "45", ".. .. .. .."], 118 + ["Djibouti", "dj", "253"], 119 + ["Dominica", "dm", "1767"], 120 + ["Dominican Republic", "do", "1", "(...) ...-....", 2, ["809", "829", "849"]], 121 + ["Ecuador", "ec", "593"], 122 + ["Egypt", "eg", "20"], 123 + ["El Salvador", "sv", "503", "....-...."], 124 + ["Equatorial Guinea", "gq", "240"], 125 + ["Eritrea", "er", "291"], 126 + ["Estonia", "ee", "372", ".... ......"], 127 + ["Ethiopia", "et", "251"], 128 + ["Fiji", "fj", "679"], 129 + ["Finland", "fi", "358", ".. ... .. .."], 130 + ["France", "fr", "33", ". .. .. .. .."], 131 + ["French Guiana", "gf", "594"], 132 + ["French Polynesia", "pf", "689"], 133 + ["Gabon", "ga", "241"], 134 + ["Gambia", "gm", "220"], 135 + ["Georgia", "ge", "995"], 136 + ["Germany", "de", "49", "... ........."], 137 + ["Ghana", "gh", "233"], 138 + ["Greece", "gr", "30"], 139 + ["Greenland", "gl", "299", ".. .. .."], 140 + ["Grenada", "gd", "1473"], 141 + ["Guadeloupe", "gp", "590", "", 0], 142 + ["Guam", "gu", "1671"], 143 + ["Guatemala", "gt", "502", "....-...."], 144 + ["Guinea", "gn", "224"], 145 + ["Guinea-Bissau", "gw", "245"], 146 + ["Guyana", "gy", "592"], 147 + ["Haiti", "ht", "509", "....-...."], 148 + ["Honduras", "hn", "504"], 149 + ["Hong Kong", "hk", "852", ".... ...."], 150 + ["Hungary", "hu", "36"], 151 + ["Iceland", "is", "354", "... ...."], 152 + ["India", "in", "91", ".....-....."], 153 + ["Indonesia", "id", "62"], 154 + ["Iran", "ir", "98", "... ... ...."], 155 + ["Iraq", "iq", "964"], 156 + ["Ireland", "ie", "353", ".. ......."], 157 + ["Israel", "il", "972", "... ... ...."], 158 + ["Italy", "it", "39", "... .......", 0], 159 + ["Jamaica", "jm", "1876"], 160 + ["Japan", "jp", "81", ".. .... ...."], 161 + ["Jordan", "jo", "962"], 162 + ["Kazakhstan", "kz", "7", "... ...-..-..", 0], 163 + ["Kenya", "ke", "254"], 164 + ["Kiribati", "ki", "686"], 165 + ["Kosovo", "xk", "383"], 166 + ["Kuwait", "kw", "965"], 167 + ["Kyrgyzstan", "kg", "996", "... ... ..."], 168 + ["Laos", "la", "856"], 169 + ["Latvia", "lv", "371", ".. ... ..."], 170 + ["Lebanon", "lb", "961"], 171 + ["Lesotho", "ls", "266"], 172 + ["Liberia", "lr", "231"], 173 + ["Libya", "ly", "218"], 174 + ["Liechtenstein", "li", "423"], 175 + ["Lithuania", "lt", "370"], 176 + ["Luxembourg", "lu", "352"], 177 + ["Macau", "mo", "853"], 178 + ["Macedonia", "mk", "389"], 179 + ["Madagascar", "mg", "261"], 180 + ["Malawi", "mw", "265"], 181 + ["Malaysia", "my", "60", "..-....-...."], 182 + ["Maldives", "mv", "960"], 183 + ["Mali", "ml", "223"], 184 + ["Malta", "mt", "356"], 185 + ["Marshall Islands", "mh", "692"], 186 + ["Martinique", "mq", "596"], 187 + ["Mauritania", "mr", "222"], 188 + ["Mauritius", "mu", "230"], 189 + ["Mexico", "mx", "52", "... ... ....", 0], 190 + ["Micronesia", "fm", "691"], 191 + ["Moldova", "md", "373", "(..) ..-..-.."], 192 + ["Monaco", "mc", "377"], 193 + ["Mongolia", "mn", "976"], 194 + ["Montenegro", "me", "382"], 195 + ["Morocco", "ma", "212"], 196 + ["Mozambique", "mz", "258"], 197 + ["Myanmar", "mm", "95"], 198 + ["Namibia", "na", "264"], 199 + ["Nauru", "nr", "674"], 200 + ["Nepal", "np", "977"], 201 + [ 202 + "Netherlands", 203 + "nl", 204 + "31", 205 + { 206 + "/^06/": "(.). .........", 207 + "/^6/": ". .........", 208 + "/^0(10|13|14|15|20|23|24|26|30|33|35|36|38|40|43|44|45|46|50|53|55|58|70|71|72|73|74|75|76|77|78|79|82|84|85|87|88|91)/": 209 + "(.).. ........", 210 + "/^(10|13|14|15|20|23|24|26|30|33|35|36|38|40|43|44|45|46|50|53|55|58|70|71|72|73|74|75|76|77|78|79|82|84|85|87|88|91)/": 211 + ".. ........", 212 + "/^0/": "(.)... .......", 213 + default: "... .......", 214 + }, 215 + ], 216 + ["New Caledonia", "nc", "687"], 217 + ["New Zealand", "nz", "64", "...-...-...."], 218 + ["Nicaragua", "ni", "505"], 219 + ["Niger", "ne", "227"], 220 + ["Nigeria", "ng", "234"], 221 + ["North Korea", "kp", "850"], 222 + ["Norway", "no", "47", "... .. ..."], 223 + ["Oman", "om", "968"], 224 + ["Pakistan", "pk", "92", "...-......."], 225 + ["Palau", "pw", "680"], 226 + ["Palestine", "ps", "970"], 227 + ["Panama", "pa", "507"], 228 + ["Papua New Guinea", "pg", "675"], 229 + ["Paraguay", "py", "595"], 230 + ["Peru", "pe", "51"], 231 + ["Philippines", "ph", "63", "... ... ...."], 232 + ["Poland", "pl", "48", "...-...-..."], 233 + ["Portugal", "pt", "351"], 234 + ["Puerto Rico", "pr", "1", "(...) ...-....", 3, ["787", "939"]], 235 + ["Qatar", "qa", "974"], 236 + ["Réunion", "re", "262"], 237 + ["Romania", "ro", "40"], 238 + ["Russia", "ru", "7", "(...) ...-..-..", 1], 239 + ["Rwanda", "rw", "250"], 240 + ["Saint Kitts and Nevis", "kn", "1869"], 241 + ["Saint Lucia", "lc", "1758"], 242 + ["Saint Vincent and the Grenadines", "vc", "1784"], 243 + ["Samoa", "ws", "685"], 244 + ["San Marino", "sm", "378"], 245 + ["São Tomé and Príncipe", "st", "239"], 246 + ["Saudi Arabia", "sa", "966"], 247 + ["Senegal", "sn", "221"], 248 + ["Serbia", "rs", "381"], 249 + ["Seychelles", "sc", "248"], 250 + ["Sierra Leone", "sl", "232"], 251 + ["Singapore", "sg", "65", "....-...."], 252 + ["Slovakia", "sk", "421"], 253 + ["Slovenia", "si", "386"], 254 + ["Solomon Islands", "sb", "677"], 255 + ["Somalia", "so", "252"], 256 + ["South Africa", "za", "27"], 257 + ["South Korea", "kr", "82", "... .... ...."], 258 + ["South Sudan", "ss", "211"], 259 + ["Spain", "es", "34", "... ... ..."], 260 + ["Sri Lanka", "lk", "94"], 261 + ["Sudan", "sd", "249"], 262 + ["Suriname", "sr", "597"], 263 + ["Swaziland", "sz", "268"], 264 + ["Sweden", "se", "46", "... ... ..."], 265 + ["Switzerland", "ch", "41", ".. ... .. .."], 266 + ["Syria", "sy", "963"], 267 + ["Taiwan", "tw", "886"], 268 + ["Tajikistan", "tj", "992"], 269 + ["Tanzania", "tz", "255"], 270 + ["Thailand", "th", "66"], 271 + ["Timor-Leste", "tl", "670"], 272 + ["Togo", "tg", "228"], 273 + ["Tonga", "to", "676"], 274 + ["Trinidad and Tobago", "tt", "1868"], 275 + ["Tunisia", "tn", "216"], 276 + ["Turkey", "tr", "90", "... ... .. .."], 277 + ["Turkmenistan", "tm", "993"], 278 + ["Tuvalu", "tv", "688"], 279 + ["Uganda", "ug", "256"], 280 + ["Ukraine", "ua", "380", "(..) ... .. .."], 281 + ["United Arab Emirates", "ae", "971"], 282 + ["United Kingdom", "gb", "44", ".... ......"], 283 + ["United States", "us", "1", "(...) ...-....", 0], 284 + ["Uruguay", "uy", "598"], 285 + ["Uzbekistan", "uz", "998", ".. ... .. .."], 286 + ["Vanuatu", "vu", "678"], 287 + ["Vatican City", "va", "39", ".. .... ....", 1], 288 + ["Venezuela", "ve", "58"], 289 + ["Vietnam", "vn", "84"], 290 + ["Yemen", "ye", "967"], 291 + ["Zambia", "zm", "260"], 292 + ["Zimbabwe", "zw", "263"], 293 + ]; 294 + 295 + type BaseCountryData = [ 296 + string, // country name 297 + string, // iso2 code 298 + string, // international dial code 299 + ]; 300 + 301 + type FormatConfig = Record<string, string> & { 302 + default: string; // can pass any string, but "default" key is required 303 + }; 304 + 305 + type CountryDataWithFormat = [ 306 + ...BaseCountryData, 307 + FormatConfig | string, // format 308 + ]; 309 + 310 + type CountryDataWithOrder = [ 311 + ...CountryDataWithFormat, 312 + number, // order priority 313 + ]; 314 + 315 + type CountryDataAreaCodes = [ 316 + ...CountryDataWithOrder, 317 + string[], // area codes 318 + ]; 319 + 320 + export type CountryData = 321 + | BaseCountryData 322 + | CountryDataWithFormat 323 + | CountryDataWithOrder 324 + | CountryDataAreaCodes;
+12
src/hooks/useRSVPData.ts
··· 1 + import { getRSVPData } from "actions/getRSVPData"; 2 + import { useReplicache } from "src/replicache"; 3 + import useSWR from "swr"; 4 + 5 + export function useRSVPData() { 6 + let { permission_token } = useReplicache(); 7 + return useSWR(`identity`, () => 8 + getRSVPData( 9 + permission_token.permission_token_rights.map((pr) => pr.entity_set), 10 + ), 11 + ); 12 + }
+1
src/replicache/attributes.ts
··· 237 237 type: "block-type-union"; 238 238 value: 239 239 | "datetime" 240 + | "rsvp" 240 241 | "text" 241 242 | "image" 242 243 | "card"
+66 -1
supabase/database.types.ts
··· 323 323 }, 324 324 ] 325 325 } 326 + phone_number_auth_tokens: { 327 + Row: { 328 + confirmation_code: string 329 + confirmed: boolean 330 + country_code: string 331 + created_at: string 332 + id: string 333 + phone_number: string 334 + } 335 + Insert: { 336 + confirmation_code: string 337 + confirmed?: boolean 338 + country_code: string 339 + created_at?: string 340 + id?: string 341 + phone_number: string 342 + } 343 + Update: { 344 + confirmation_code?: string 345 + confirmed?: boolean 346 + country_code?: string 347 + created_at?: string 348 + id?: string 349 + phone_number?: string 350 + } 351 + Relationships: [] 352 + } 353 + phone_rsvps_to_entity: { 354 + Row: { 355 + country_code: string 356 + created_at: string 357 + entity: string 358 + id: string 359 + name: string 360 + phone_number: string 361 + status: Database["public"]["Enums"]["rsvp_status"] 362 + } 363 + Insert: { 364 + country_code: string 365 + created_at?: string 366 + entity: string 367 + id?: string 368 + name?: string 369 + phone_number: string 370 + status: Database["public"]["Enums"]["rsvp_status"] 371 + } 372 + Update: { 373 + country_code?: string 374 + created_at?: string 375 + entity?: string 376 + id?: string 377 + name?: string 378 + phone_number?: string 379 + status?: Database["public"]["Enums"]["rsvp_status"] 380 + } 381 + Relationships: [ 382 + { 383 + foreignKeyName: "phone_rsvps_to_entity_entity_fkey" 384 + columns: ["entity"] 385 + isOneToOne: false 386 + referencedRelation: "entities" 387 + referencedColumns: ["id"] 388 + }, 389 + ] 390 + } 326 391 replicache_clients: { 327 392 Row: { 328 393 client_group: string ··· 387 452 } 388 453 } 389 454 Enums: { 390 - [_ in never]: never 455 + rsvp_status: "GOING" | "NOT_GOING" | "MAYBE" 391 456 } 392 457 CompositeTypes: { 393 458 [_ in never]: never
+124
supabase/migrations/20250107212502_add_phone_auth_tables.sql
··· 1 + create type "public"."rsvp_status" as enum ('GOING', 'NOT_GOING', 'MAYBE'); 2 + 3 + create table "public"."phone_number_auth_tokens" ( 4 + "id" uuid not null default gen_random_uuid(), 5 + "created_at" timestamp with time zone not null default now(), 6 + "confirmed" boolean not null default false, 7 + "confirmation_code" text not null, 8 + "phone_number" text not null, 9 + "country_code" text not null 10 + ); 11 + 12 + 13 + alter table "public"."phone_number_auth_tokens" enable row level security; 14 + 15 + create table "public"."phone_rsvps_to_entity" ( 16 + "created_at" timestamp with time zone not null default now(), 17 + "phone_number" text not null, 18 + "country_code" text not null, 19 + "status" rsvp_status not null, 20 + "id" uuid not null default gen_random_uuid(), 21 + "entity" uuid not null, 22 + "name" text not null default ''::text 23 + ); 24 + 25 + 26 + alter table "public"."phone_rsvps_to_entity" enable row level security; 27 + 28 + CREATE UNIQUE INDEX phone_number_auth_tokens_pkey ON public.phone_number_auth_tokens USING btree (id); 29 + 30 + CREATE UNIQUE INDEX phone_rsvps_to_entity_pkey ON public.phone_rsvps_to_entity USING btree (id); 31 + 32 + CREATE UNIQUE INDEX unique_phone_number_entities ON public.phone_rsvps_to_entity USING btree (phone_number, entity); 33 + 34 + alter table "public"."phone_number_auth_tokens" add constraint "phone_number_auth_tokens_pkey" PRIMARY KEY using index "phone_number_auth_tokens_pkey"; 35 + 36 + alter table "public"."phone_rsvps_to_entity" add constraint "phone_rsvps_to_entity_pkey" PRIMARY KEY using index "phone_rsvps_to_entity_pkey"; 37 + 38 + alter table "public"."phone_rsvps_to_entity" add constraint "phone_rsvps_to_entity_entity_fkey" FOREIGN KEY (entity) REFERENCES entities(id) ON UPDATE CASCADE ON DELETE CASCADE not valid; 39 + 40 + alter table "public"."phone_rsvps_to_entity" validate constraint "phone_rsvps_to_entity_entity_fkey"; 41 + 42 + grant delete on table "public"."phone_number_auth_tokens" to "anon"; 43 + 44 + grant insert on table "public"."phone_number_auth_tokens" to "anon"; 45 + 46 + grant references on table "public"."phone_number_auth_tokens" to "anon"; 47 + 48 + grant select on table "public"."phone_number_auth_tokens" to "anon"; 49 + 50 + grant trigger on table "public"."phone_number_auth_tokens" to "anon"; 51 + 52 + grant truncate on table "public"."phone_number_auth_tokens" to "anon"; 53 + 54 + grant update on table "public"."phone_number_auth_tokens" to "anon"; 55 + 56 + grant delete on table "public"."phone_number_auth_tokens" to "authenticated"; 57 + 58 + grant insert on table "public"."phone_number_auth_tokens" to "authenticated"; 59 + 60 + grant references on table "public"."phone_number_auth_tokens" to "authenticated"; 61 + 62 + grant select on table "public"."phone_number_auth_tokens" to "authenticated"; 63 + 64 + grant trigger on table "public"."phone_number_auth_tokens" to "authenticated"; 65 + 66 + grant truncate on table "public"."phone_number_auth_tokens" to "authenticated"; 67 + 68 + grant update on table "public"."phone_number_auth_tokens" to "authenticated"; 69 + 70 + grant delete on table "public"."phone_number_auth_tokens" to "service_role"; 71 + 72 + grant insert on table "public"."phone_number_auth_tokens" to "service_role"; 73 + 74 + grant references on table "public"."phone_number_auth_tokens" to "service_role"; 75 + 76 + grant select on table "public"."phone_number_auth_tokens" to "service_role"; 77 + 78 + grant trigger on table "public"."phone_number_auth_tokens" to "service_role"; 79 + 80 + grant truncate on table "public"."phone_number_auth_tokens" to "service_role"; 81 + 82 + grant update on table "public"."phone_number_auth_tokens" to "service_role"; 83 + 84 + grant delete on table "public"."phone_rsvps_to_entity" to "anon"; 85 + 86 + grant insert on table "public"."phone_rsvps_to_entity" to "anon"; 87 + 88 + grant references on table "public"."phone_rsvps_to_entity" to "anon"; 89 + 90 + grant select on table "public"."phone_rsvps_to_entity" to "anon"; 91 + 92 + grant trigger on table "public"."phone_rsvps_to_entity" to "anon"; 93 + 94 + grant truncate on table "public"."phone_rsvps_to_entity" to "anon"; 95 + 96 + grant update on table "public"."phone_rsvps_to_entity" to "anon"; 97 + 98 + grant delete on table "public"."phone_rsvps_to_entity" to "authenticated"; 99 + 100 + grant insert on table "public"."phone_rsvps_to_entity" to "authenticated"; 101 + 102 + grant references on table "public"."phone_rsvps_to_entity" to "authenticated"; 103 + 104 + grant select on table "public"."phone_rsvps_to_entity" to "authenticated"; 105 + 106 + grant trigger on table "public"."phone_rsvps_to_entity" to "authenticated"; 107 + 108 + grant truncate on table "public"."phone_rsvps_to_entity" to "authenticated"; 109 + 110 + grant update on table "public"."phone_rsvps_to_entity" to "authenticated"; 111 + 112 + grant delete on table "public"."phone_rsvps_to_entity" to "service_role"; 113 + 114 + grant insert on table "public"."phone_rsvps_to_entity" to "service_role"; 115 + 116 + grant references on table "public"."phone_rsvps_to_entity" to "service_role"; 117 + 118 + grant select on table "public"."phone_rsvps_to_entity" to "service_role"; 119 + 120 + grant trigger on table "public"."phone_rsvps_to_entity" to "service_role"; 121 + 122 + grant truncate on table "public"."phone_rsvps_to_entity" to "service_role"; 123 + 124 + grant update on table "public"."phone_rsvps_to_entity" to "service_role";