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 import { YJSFragmentToString } from "components/Blocks/TextBlock/RenderYJSFragment"; 10 import { Leaflet } from "./Leaflet"; 11 import { scanIndexLocal } from "src/replicache/utils"; 12 13 export const preferredRegion = ["sfo1"]; 14 export const dynamic = "force-dynamic"; ··· 48 </div> 49 ); 50 51 - let { data } = await supabase.rpc("get_facts", { 52 - root: rootEntity, 53 - }); 54 let initialFacts = (data as unknown as Fact<keyof typeof Attributes>[]) || []; 55 return ( 56 - <Leaflet 57 - initialFacts={initialFacts} 58 - leaflet_id={rootEntity} 59 - token={res.data} 60 - /> 61 ); 62 } 63
··· 9 import { YJSFragmentToString } from "components/Blocks/TextBlock/RenderYJSFragment"; 10 import { Leaflet } from "./Leaflet"; 11 import { scanIndexLocal } from "src/replicache/utils"; 12 + import { getRSVPData } from "actions/getRSVPData"; 13 + import { RSVPDataProvider } from "components/RSVPDataProvider"; 14 15 export const preferredRegion = ["sfo1"]; 16 export const dynamic = "force-dynamic"; ··· 50 </div> 51 ); 52 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 + ]); 59 let initialFacts = (data as unknown as Fact<keyof typeof Attributes>[]) || []; 60 return ( 61 + <RSVPDataProvider data={identity_data}> 62 + <Leaflet 63 + initialFacts={initialFacts} 64 + leaflet_id={rootEntity} 65 + token={res.data} 66 + /> 67 + </RSVPDataProvider> 68 ); 69 } 70
+3
app/globals.css
··· 180 @apply px-1; 181 @apply py-0.5; 182 @apply hover:border-tertiary; 183 @apply focus:border-tertiary; 184 @apply focus:outline; 185 @apply focus:outline-tertiary; 186 @apply focus:outline-2; 187 @apply focus:outline-offset-1; 188 @apply focus-within:border-tertiary; 189 @apply focus-within:outline; 190 @apply focus-within:outline-tertiary; 191 @apply focus-within:outline-2; 192 @apply focus-within:outline-offset-1; 193 @apply disabled:border-border-light; 194 @apply disabled:bg-border-light; 195 @apply disabled:text-tertiary;
··· 180 @apply px-1; 181 @apply py-0.5; 182 @apply hover:border-tertiary; 183 + 184 @apply focus:border-tertiary; 185 @apply focus:outline; 186 @apply focus:outline-tertiary; 187 @apply focus:outline-2; 188 @apply focus:outline-offset-1; 189 + 190 @apply focus-within:border-tertiary; 191 @apply focus-within:outline; 192 @apply focus-within:outline-tertiary; 193 @apply focus-within:outline-2; 194 @apply focus-within:outline-offset-1; 195 + 196 @apply disabled:border-border-light; 197 @apply disabled:bg-border-light; 198 @apply disabled:text-tertiary;
+6 -1
app/layout.tsx
··· 6 import localFont from "next/font/local"; 7 import { PopUpProvider } from "components/Toast"; 8 import { IdentityProviderServer } from "components/IdentityProviderServer"; 9 10 export const metadata = { 11 title: "Leaflet", ··· 44 }: { 45 children: React.ReactNode; 46 }) { 47 return ( 48 <html lang="en" className={`${quattro.variable}`}> 49 <body> ··· 65 <InitialPageLoad> 66 <PopUpProvider> 67 <IdentityProviderServer> 68 - <ViewportSizeLayout>{children}</ViewportSizeLayout> 69 </IdentityProviderServer> 70 </PopUpProvider> 71 </InitialPageLoad>
··· 6 import localFont from "next/font/local"; 7 import { PopUpProvider } from "components/Toast"; 8 import { IdentityProviderServer } from "components/IdentityProviderServer"; 9 + import { headers } from "next/headers"; 10 + import { IPLocationProvider } from "components/Providers/IPLocationProvider"; 11 12 export const metadata = { 13 title: "Leaflet", ··· 46 }: { 47 children: React.ReactNode; 48 }) { 49 + let ipLocation = headers().get("X-Vercel-IP-Country"); 50 return ( 51 <html lang="en" className={`${quattro.variable}`}> 52 <body> ··· 68 <InitialPageLoad> 69 <PopUpProvider> 70 <IdentityProviderServer> 71 + <IPLocationProvider country={ipLocation}> 72 + <ViewportSizeLayout>{children}</ViewportSizeLayout> 73 + </IPLocationProvider> 74 </IdentityProviderServer> 75 </PopUpProvider> 76 </InitialPageLoad>
+2
components/Blocks/Block.tsx
··· 27 import { Media } from "components/Media"; 28 import { useIsMobile } from "src/hooks/isMobile"; 29 import { DateTimeBlock } from "./DateTimeBlock"; 30 import { elementId } from "src/utils/elementId"; 31 32 export type Block = { ··· 160 embed: EmbedBlock, 161 mailbox: MailboxBlock, 162 datetime: DateTimeBlock, 163 }; 164 165 export const BlockMultiselectIndicator = (props: BlockProps) => {
··· 27 import { Media } from "components/Media"; 28 import { useIsMobile } from "src/hooks/isMobile"; 29 import { DateTimeBlock } from "./DateTimeBlock"; 30 + import { RSVPBlock } from "./RSVPBlock"; 31 import { elementId } from "src/utils/elementId"; 32 33 export type Block = { ··· 161 embed: EmbedBlock, 162 mailbox: MailboxBlock, 163 datetime: DateTimeBlock, 164 + rsvp: RSVPBlock, 165 }; 166 167 export const BlockMultiselectIndicator = (props: BlockProps) => {
+8 -1
components/Blocks/BlockCommands.tsx
··· 12 LinkSmall, 13 BlockEmbedSmall, 14 BlockCalendarSmall, 15 } from "components/Icons"; 16 import { generateKeyBetween } from "fractional-indexing"; 17 import { focusPage } from "components/Pages"; ··· 198 // EVENT STUFF 199 200 { 201 name: "Date and Time", 202 icon: <BlockCalendarSmall />, 203 - type: "block", 204 onSelect: (rep, props) => createBlockWithType(rep, props, "datetime"), 205 }, 206
··· 12 LinkSmall, 13 BlockEmbedSmall, 14 BlockCalendarSmall, 15 + RSVPSmall, 16 } from "components/Icons"; 17 import { generateKeyBetween } from "fractional-indexing"; 18 import { focusPage } from "components/Pages"; ··· 199 // EVENT STUFF 200 201 { 202 + name: "RSVP", 203 + icon: <RSVPSmall />, 204 + type: "event", 205 + onSelect: (rep, props) => createBlockWithType(rep, props, "rsvp"), 206 + }, 207 + { 208 name: "Date and Time", 209 icon: <BlockCalendarSmall />, 210 + type: "event", 211 onSelect: (rep, props) => createBlockWithType(rep, props, "datetime"), 212 }, 213
+4 -4
components/Blocks/EmbedBlock.tsx
··· 128 }, 129 }); 130 }; 131 - let smoke = useSmoker(); 132 133 return ( 134 <div> ··· 153 if (!linkValue) return; 154 if (!isUrl(linkValue)) { 155 let rect = e.currentTarget.getBoundingClientRect(); 156 - smoke({ 157 error: true, 158 text: "invalid url!", 159 position: { x: rect.left, y: rect.top - 8 }, ··· 170 onMouseDown={(e) => { 171 e.preventDefault(); 172 if (!linkValue || linkValue === "") { 173 - smoke({ 174 error: true, 175 text: "no url!", 176 position: { x: e.clientX, y: e.clientY }, ··· 178 return; 179 } 180 if (!isUrl(linkValue)) { 181 - smoke({ 182 error: true, 183 text: "invalid url!", 184 position: { x: e.clientX, y: e.clientY },
··· 128 }, 129 }); 130 }; 131 + let smoker = useSmoker(); 132 133 return ( 134 <div> ··· 153 if (!linkValue) return; 154 if (!isUrl(linkValue)) { 155 let rect = e.currentTarget.getBoundingClientRect(); 156 + smoker({ 157 error: true, 158 text: "invalid url!", 159 position: { x: rect.left, y: rect.top - 8 }, ··· 170 onMouseDown={(e) => { 171 e.preventDefault(); 172 if (!linkValue || linkValue === "") { 173 + smoker({ 174 error: true, 175 text: "no url!", 176 position: { x: e.clientX, y: e.clientY }, ··· 178 return; 179 } 180 if (!isUrl(linkValue)) { 181 + smoker({ 182 error: true, 183 text: "invalid url!", 184 position: { x: e.clientX, y: e.clientY },
+10 -7
components/Blocks/ExternalLinkBlock.tsx
··· 42 id={props.preview ? undefined : elementId.block(props.entityID).input} 43 className={` 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"} 48 ${props.pageType === "canvas" && "bg-bg-page"}`} 49 onMouseDown={() => { 50 focusBlock( ··· 153 { type: "start" }, 154 ); 155 }; 156 - let smoke = useSmoker(); 157 158 return ( 159 <div className={`max-w-sm flex gap-2 rounded-md text-secondary`}> ··· 178 if (!linkValue) return; 179 if (!isUrl(linkValue)) { 180 let rect = e.currentTarget.getBoundingClientRect(); 181 - smoke({ 182 error: true, 183 text: "invalid url!", 184 position: { x: rect.left, y: rect.top - 8 }, ··· 196 onMouseDown={(e) => { 197 e.preventDefault(); 198 if (!linkValue || linkValue === "") { 199 - smoke({ 200 error: true, 201 text: "no url!", 202 position: { x: e.clientX, y: e.clientY }, ··· 204 return; 205 } 206 if (!isUrl(linkValue)) { 207 - smoke({ 208 error: true, 209 text: "invalid url!", 210 position: { x: e.clientX, y: e.clientY },
··· 42 id={props.preview ? undefined : elementId.block(props.entityID).input} 43 className={` 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"} 48 ${props.pageType === "canvas" && "bg-bg-page"}`} 49 onMouseDown={() => { 50 focusBlock( ··· 153 { type: "start" }, 154 ); 155 }; 156 + let smoker = useSmoker(); 157 158 return ( 159 <div className={`max-w-sm flex gap-2 rounded-md text-secondary`}> ··· 178 if (!linkValue) return; 179 if (!isUrl(linkValue)) { 180 let rect = e.currentTarget.getBoundingClientRect(); 181 + smoker({ 182 + alignOnMobile: "left", 183 error: true, 184 text: "invalid url!", 185 position: { x: rect.left, y: rect.top - 8 }, ··· 197 onMouseDown={(e) => { 198 e.preventDefault(); 199 if (!linkValue || linkValue === "") { 200 + smoker({ 201 + alignOnMobile: "left", 202 error: true, 203 text: "no url!", 204 position: { x: e.clientX, y: e.clientY }, ··· 206 return; 207 } 208 if (!isUrl(linkValue)) { 209 + smoker({ 210 + alignOnMobile: "left", 211 error: true, 212 text: "invalid url!", 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 el.tagName === "TEXTAREA" || 52 el.contentEditable === "true" 53 ) { 54 - if ((el as HTMLInputElement).value !== "") return; 55 } 56 57 command?.({
··· 51 el.tagName === "TEXTAREA" || 52 el.contentEditable === "true" 53 ) { 54 + if ((el as HTMLInputElement).value !== "" || e.key === "Tab") return; 55 } 56 57 command?.({
+53
components/Buttons.tsx
··· 47 }); 48 ButtonPrimary.displayName = "ButtonPrimary"; 49 50 export const HoverButton = (props: { 51 id?: string; 52 icon: React.ReactNode;
··· 47 }); 48 ButtonPrimary.displayName = "ButtonPrimary"; 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 + 103 export const HoverButton = (props: { 104 id?: string; 105 icon: React.ReactNode;
+1 -1
components/Checkbox.tsx
··· 9 }) { 10 return ( 11 <label 12 - className={`flex gap-2 items-start cursor-pointer shrink-0 ${props.checked ? "text-primary font-bold " : " text-tertiary font-normal"}`} 13 > 14 <input 15 type="checkbox"
··· 9 }) { 10 return ( 11 <label 12 + className={`flex w-full gap-2 items-start cursor-pointer ${props.checked ? "text-primary font-bold " : " text-tertiary font-normal"}`} 13 > 14 <input 15 type="checkbox"
+40
components/Icons.tsx
··· 485 ); 486 }; 487 488 export const AccountSmall = (props: Props) => { 489 return ( 490 <svg ··· 504 </svg> 505 ); 506 }; 507 export const ShareSmall = (props: Props) => { 508 return ( 509 <svg ··· 1310 fillRule="evenodd" 1311 clipRule="evenodd" 1312 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" 1313 fill="currentColor" 1314 /> 1315 </svg>
··· 485 ); 486 }; 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 + 508 export const AccountSmall = (props: Props) => { 509 return ( 510 <svg ··· 524 </svg> 525 ); 526 }; 527 + 528 export const ShareSmall = (props: Props) => { 529 return ( 530 <svg ··· 1331 fillRule="evenodd" 1332 clipRule="evenodd" 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" 1353 fill="currentColor" 1354 /> 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 ); 112 smoker({ 113 position: { 114 - x: rect ? rect.left + 80 : 0, 115 y: rect ? rect.top + 26 : 0, 116 }, 117 text: props.smokerText,
··· 111 ); 112 smoker({ 113 position: { 114 + x: rect ? rect.left + (rect.right - rect.left) / 2 : 0, 115 y: rect ? rect.top + 26 : 0, 116 }, 117 text: props.smokerText,
+15 -5
components/Toast.tsx
··· 20 text: React.ReactNode; 21 static?: boolean; 22 error?: boolean; 23 }; 24 25 type Smokes = Array<Smoke & { key: string }>; ··· 76 error={smoke.error} 77 key={smoke.key} 78 static={smoke.static} 79 > 80 {smoke.text} 81 </Smoke> ··· 136 y: number; 137 error?: boolean; 138 static?: boolean; 139 }> 140 > = (props) => { 141 return ( 142 <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 - }`} 148 > 149 <style jsx>{` 150 .smoke {
··· 20 text: React.ReactNode; 21 static?: boolean; 22 error?: boolean; 23 + alignOnMobile?: "left" | "right" | "center" | undefined; 24 }; 25 26 type Smokes = Array<Smoke & { key: string }>; ··· 77 error={smoke.error} 78 key={smoke.key} 79 static={smoke.static} 80 + alignOnMobile={smoke.alignOnMobile} 81 > 82 {smoke.text} 83 </Smoke> ··· 138 y: number; 139 error?: boolean; 140 static?: boolean; 141 + alignOnMobile?: "left" | "right" | "center" | undefined; 142 }> 143 > = (props) => { 144 return ( 145 <div 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 + }`} 158 > 159 <style jsx>{` 160 .smoke {
+23 -15
drizzle/relations.ts
··· 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"; 3 4 export const entitiesRelations = relations(entities, ({one, many}) => ({ 5 entity_set: one(entity_sets, { 6 fields: [entities.set], 7 references: [entity_sets.id] 8 }), 9 - facts: many(facts), 10 permission_tokens: many(permission_tokens), 11 email_subscriptions_to_entities: many(email_subscriptions_to_entity), 12 })); 13 14 export const entity_setsRelations = relations(entity_sets, ({many}) => ({ ··· 16 permission_token_rights: many(permission_token_rights), 17 })); 18 19 - export const factsRelations = relations(facts, ({one}) => ({ 20 entity: one(entities, { 21 - fields: [facts.entity], 22 references: [entities.id] 23 }), 24 })); 25 26 export const identitiesRelations = relations(identities, ({one, many}) => ({ ··· 32 permission_token_on_homepages: many(permission_token_on_homepage), 33 })); 34 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 export const email_subscriptions_to_entityRelations = relations(email_subscriptions_to_entity, ({one}) => ({ 47 entity: one(entities, { 48 fields: [email_subscriptions_to_entity.entity], ··· 58 identity: one(identities, { 59 fields: [email_auth_tokens.identity], 60 references: [identities.id] 61 }), 62 })); 63
··· 1 import { relations } from "drizzle-orm/relations"; 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 + })); 10 11 export const entitiesRelations = relations(entities, ({one, many}) => ({ 12 + facts: many(facts), 13 entity_set: one(entity_sets, { 14 fields: [entities.set], 15 references: [entity_sets.id] 16 }), 17 permission_tokens: many(permission_tokens), 18 email_subscriptions_to_entities: many(email_subscriptions_to_entity), 19 + phone_rsvps_to_entities: many(phone_rsvps_to_entity), 20 })); 21 22 export const entity_setsRelations = relations(entity_sets, ({many}) => ({ ··· 24 permission_token_rights: many(permission_token_rights), 25 })); 26 27 + export const permission_tokensRelations = relations(permission_tokens, ({one, many}) => ({ 28 entity: one(entities, { 29 + fields: [permission_tokens.root_entity], 30 references: [entities.id] 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), 36 })); 37 38 export const identitiesRelations = relations(identities, ({one, many}) => ({ ··· 44 permission_token_on_homepages: many(permission_token_on_homepage), 45 })); 46 47 export const email_subscriptions_to_entityRelations = relations(email_subscriptions_to_entity, ({one}) => ({ 48 entity: one(entities, { 49 fields: [email_subscriptions_to_entity.entity], ··· 59 identity: one(identities, { 60 fields: [email_auth_tokens.identity], 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] 69 }), 70 })); 71
+37 -12
drizzle/schema.ts
··· 1 - import { pgTable, foreignKey, pgEnum, uuid, timestamp, text, jsonb, bigint, boolean, primaryKey } from "drizzle-orm/pg-core" 2 import { sql } from "drizzle-orm" 3 4 export const aal_level = pgEnum("aal_level", ['aal1', 'aal2', 'aal3']) ··· 9 export const request_status = pgEnum("request_status", ['PENDING', 'SUCCESS', 'ERROR']) 10 export const key_status = pgEnum("key_status", ['default', 'valid', 'invalid', 'expired']) 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 action = pgEnum("action", ['INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', 'ERROR']) 13 export const equality_op = pgEnum("equality_op", ['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'in']) 14 15 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 export const facts = pgTable("facts", { 23 id: uuid("id").primaryKey().notNull(), 24 entity: uuid("entity").notNull().references(() => entities.id, { onDelete: "cascade", onUpdate: "restrict" } ), ··· 37 last_mutation: bigint("last_mutation", { mode: "number" }).notNull(), 38 }); 39 40 export const entity_sets = pgTable("entity_sets", { 41 id: uuid("id").defaultRandom().primaryKey().notNull(), 42 created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 43 }); 44 45 export const identities = pgTable("identities", { 46 id: uuid("id").defaultRandom().primaryKey().notNull(), 47 created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 48 home_page: uuid("home_page").notNull().references(() => permission_tokens.id, { onDelete: "cascade" } ), 49 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 57 export const email_subscriptions_to_entity = pgTable("email_subscriptions_to_entity", { ··· 71 email: text("email").notNull(), 72 confirmation_code: text("confirmation_code").notNull(), 73 identity: uuid("identity").references(() => identities.id, { onDelete: "cascade", onUpdate: "cascade" } ), 74 }); 75 76 export const permission_token_on_homepage = pgTable("permission_token_on_homepage", {
··· 1 + import { pgTable, foreignKey, pgEnum, uuid, text, jsonb, timestamp, bigint, boolean, uniqueIndex, primaryKey } from "drizzle-orm/pg-core" 2 import { sql } from "drizzle-orm" 3 4 export const aal_level = pgEnum("aal_level", ['aal1', 'aal2', 'aal3']) ··· 9 export const request_status = pgEnum("request_status", ['PENDING', 'SUCCESS', 'ERROR']) 10 export const key_status = pgEnum("key_status", ['default', 'valid', 'invalid', 'expired']) 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']) 13 export const action = pgEnum("action", ['INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', 'ERROR']) 14 export const equality_op = pgEnum("equality_op", ['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'in']) 15 16 17 export const facts = pgTable("facts", { 18 id: uuid("id").primaryKey().notNull(), 19 entity: uuid("entity").notNull().references(() => entities.id, { onDelete: "cascade", onUpdate: "restrict" } ), ··· 32 last_mutation: bigint("last_mutation", { mode: "number" }).notNull(), 33 }); 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 + 41 export const entity_sets = pgTable("entity_sets", { 42 id: uuid("id").defaultRandom().primaryKey().notNull(), 43 created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 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 + 51 export const identities = pgTable("identities", { 52 id: uuid("id").defaultRandom().primaryKey().notNull(), 53 created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 54 home_page: uuid("home_page").notNull().references(() => permission_tokens.id, { onDelete: "cascade" } ), 55 email: text("email"), 56 }); 57 58 export const email_subscriptions_to_entity = pgTable("email_subscriptions_to_entity", { ··· 72 email: text("email").notNull(), 73 confirmation_code: text("confirmation_code").notNull(), 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 + } 99 }); 100 101 export const permission_token_on_homepage = pgTable("permission_token_on_homepage", {
+204 -15
package-lock.json
··· 53 "replicache-react": "^5.0.1", 54 "swr": "^2.2.5", 55 "thumbhash": "^0.1.1", 56 "unified": "^11.0.5", 57 "unist-util-visit": "^5.0.0", 58 "uuid": "^10.0.0", ··· 6816 } 6817 }, 6818 "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==", 6822 "dependencies": { 6823 "follow-redirects": "^1.15.6", 6824 "form-data": "^4.0.0", ··· 6955 "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" 6956 } 6957 }, 6958 "node_modules/buffer-from": { 6959 "version": "1.1.2", 6960 "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", ··· 6976 "version": "1.0.7", 6977 "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", 6978 "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", 6979 - "dev": true, 6980 "dependencies": { 6981 "es-define-property": "^1.0.0", 6982 "es-errors": "^1.3.0", ··· 7397 "url": "https://github.com/sponsors/kossnocorp" 7398 } 7399 }, 7400 "node_modules/debounce": { 7401 "version": "1.2.1", 7402 "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", ··· 7440 "version": "1.1.4", 7441 "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", 7442 "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", 7443 - "dev": true, 7444 "dependencies": { 7445 "es-define-property": "^1.0.0", 7446 "es-errors": "^1.3.0", ··· 7698 "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", 7699 "dev": true 7700 }, 7701 "node_modules/electron-to-chromium": { 7702 "version": "1.4.783", 7703 "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.783.tgz", ··· 7810 "version": "1.0.0", 7811 "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", 7812 "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", 7813 - "dev": true, 7814 "dependencies": { 7815 "get-intrinsic": "^1.2.4" 7816 }, ··· 7822 "version": "1.3.0", 7823 "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 7824 "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 7825 - "dev": true, 7826 "engines": { 7827 "node": ">= 0.4" 7828 } ··· 8923 "version": "1.2.4", 8924 "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", 8925 "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", 8926 - "dev": true, 8927 "dependencies": { 8928 "es-errors": "^1.3.0", 8929 "function-bind": "^1.1.2", ··· 9105 "version": "1.0.1", 9106 "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", 9107 "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", 9108 - "dev": true, 9109 "dependencies": { 9110 "get-intrinsic": "^1.1.3" 9111 }, ··· 9156 "version": "1.0.2", 9157 "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", 9158 "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", 9159 - "dev": true, 9160 "dependencies": { 9161 "es-define-property": "^1.0.0" 9162 }, ··· 9168 "version": "1.0.3", 9169 "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", 9170 "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", 9171 - "dev": true, 9172 "engines": { 9173 "node": ">= 0.4" 9174 }, ··· 9180 "version": "1.0.3", 9181 "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 9182 "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 9183 - "dev": true, 9184 "engines": { 9185 "node": ">= 0.4" 9186 }, ··· 10212 "json5": "lib/cli.js" 10213 } 10214 }, 10215 "node_modules/jsx-ast-utils": { 10216 "version": "3.3.5", 10217 "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", ··· 10225 }, 10226 "engines": { 10227 "node": ">=4.0" 10228 } 10229 }, 10230 "node_modules/keyv": { ··· 10322 "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", 10323 "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" 10324 }, 10325 "node_modules/lodash.merge": { 10326 "version": "4.6.2", 10327 "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 10328 "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" 10329 }, 10330 "node_modules/lodash.throttle": { 10331 "version": "4.1.1", ··· 11882 "version": "1.13.1", 11883 "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", 11884 "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", 11885 - "dev": true, 11886 "funding": { 11887 "url": "https://github.com/sponsors/ljharb" 11888 } ··· 12533 "dev": true, 12534 "engines": { 12535 "node": ">=6" 12536 } 12537 }, 12538 "node_modules/queue-microtask": { ··· 13297 "url": "https://github.com/sponsors/ljharb" 13298 } 13299 }, 13300 "node_modules/safe-regex-test": { 13301 "version": "1.0.3", 13302 "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", ··· 13321 "dependencies": { 13322 "loose-envify": "^1.1.0" 13323 } 13324 }, 13325 "node_modules/selfsigned": { 13326 "version": "2.4.1", ··· 13355 "version": "1.2.2", 13356 "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", 13357 "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", 13358 - "dev": true, 13359 "dependencies": { 13360 "define-data-property": "^1.1.4", 13361 "es-errors": "^1.3.0", ··· 13413 "version": "1.0.6", 13414 "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", 13415 "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", 13416 - "dev": true, 13417 "dependencies": { 13418 "call-bind": "^1.0.7", 13419 "es-errors": "^1.3.0", ··· 14037 "version": "2.6.2", 14038 "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", 14039 "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" 14040 }, 14041 "node_modules/type": { 14042 "version": "2.7.2", ··· 15100 "utf-8-validate": { 15101 "optional": true 15102 } 15103 } 15104 }, 15105 "node_modules/xxhash-wasm": {
··· 53 "replicache-react": "^5.0.1", 54 "swr": "^2.2.5", 55 "thumbhash": "^0.1.1", 56 + "twilio": "^5.3.7", 57 "unified": "^11.0.5", 58 "unist-util-visit": "^5.0.0", 59 "uuid": "^10.0.0", ··· 6817 } 6818 }, 6819 "node_modules/axios": { 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", 6824 "dependencies": { 6825 "follow-redirects": "^1.15.6", 6826 "form-data": "^4.0.0", ··· 6957 "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" 6958 } 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 + }, 6966 "node_modules/buffer-from": { 6967 "version": "1.1.2", 6968 "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", ··· 6984 "version": "1.0.7", 6985 "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", 6986 "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", 6987 "dependencies": { 6988 "es-define-property": "^1.0.0", 6989 "es-errors": "^1.3.0", ··· 7404 "url": "https://github.com/sponsors/kossnocorp" 7405 } 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 + }, 7413 "node_modules/debounce": { 7414 "version": "1.2.1", 7415 "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", ··· 7453 "version": "1.1.4", 7454 "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", 7455 "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", 7456 "dependencies": { 7457 "es-define-property": "^1.0.0", 7458 "es-errors": "^1.3.0", ··· 7710 "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", 7711 "dev": true 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 + }, 7722 "node_modules/electron-to-chromium": { 7723 "version": "1.4.783", 7724 "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.783.tgz", ··· 7831 "version": "1.0.0", 7832 "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", 7833 "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", 7834 "dependencies": { 7835 "get-intrinsic": "^1.2.4" 7836 }, ··· 7842 "version": "1.3.0", 7843 "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 7844 "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 7845 "engines": { 7846 "node": ">= 0.4" 7847 } ··· 8942 "version": "1.2.4", 8943 "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", 8944 "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", 8945 "dependencies": { 8946 "es-errors": "^1.3.0", 8947 "function-bind": "^1.1.2", ··· 9123 "version": "1.0.1", 9124 "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", 9125 "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", 9126 "dependencies": { 9127 "get-intrinsic": "^1.1.3" 9128 }, ··· 9173 "version": "1.0.2", 9174 "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", 9175 "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", 9176 "dependencies": { 9177 "es-define-property": "^1.0.0" 9178 }, ··· 9184 "version": "1.0.3", 9185 "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", 9186 "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", 9187 "engines": { 9188 "node": ">= 0.4" 9189 }, ··· 9195 "version": "1.0.3", 9196 "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 9197 "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 9198 "engines": { 9199 "node": ">= 0.4" 9200 }, ··· 10226 "json5": "lib/cli.js" 10227 } 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 + }, 10251 "node_modules/jsx-ast-utils": { 10252 "version": "3.3.5", 10253 "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", ··· 10261 }, 10262 "engines": { 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" 10285 } 10286 }, 10287 "node_modules/keyv": { ··· 10379 "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", 10380 "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" 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 + }, 10418 "node_modules/lodash.merge": { 10419 "version": "4.6.2", 10420 "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 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" 10428 }, 10429 "node_modules/lodash.throttle": { 10430 "version": "4.1.1", ··· 11981 "version": "1.13.1", 11982 "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", 11983 "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", 11984 "funding": { 11985 "url": "https://github.com/sponsors/ljharb" 11986 } ··· 12631 "dev": true, 12632 "engines": { 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" 12649 } 12650 }, 12651 "node_modules/queue-microtask": { ··· 13410 "url": "https://github.com/sponsors/ljharb" 13411 } 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 + }, 13433 "node_modules/safe-regex-test": { 13434 "version": "1.0.3", 13435 "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", ··· 13454 "dependencies": { 13455 "loose-envify": "^1.1.0" 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" 13463 }, 13464 "node_modules/selfsigned": { 13465 "version": "2.4.1", ··· 13494 "version": "1.2.2", 13495 "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", 13496 "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", 13497 "dependencies": { 13498 "define-data-property": "^1.1.4", 13499 "es-errors": "^1.3.0", ··· 13551 "version": "1.0.6", 13552 "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", 13553 "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", 13554 "dependencies": { 13555 "call-bind": "^1.0.7", 13556 "es-errors": "^1.3.0", ··· 14174 "version": "2.6.2", 14175 "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", 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 + } 14220 }, 14221 "node_modules/type": { 14222 "version": "2.7.2", ··· 15280 "utf-8-validate": { 15281 "optional": true 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" 15292 } 15293 }, 15294 "node_modules/xxhash-wasm": {
+1
package.json
··· 55 "replicache-react": "^5.0.1", 56 "swr": "^2.2.5", 57 "thumbhash": "^0.1.1", 58 "unified": "^11.0.5", 59 "unist-util-visit": "^5.0.0", 60 "uuid": "^10.0.0",
··· 55 "replicache-react": "^5.0.1", 56 "swr": "^2.2.5", 57 "thumbhash": "^0.1.1", 58 + "twilio": "^5.3.7", 59 "unified": "^11.0.5", 60 "unist-util-visit": "^5.0.0", 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 type: "block-type-union"; 238 value: 239 | "datetime" 240 | "text" 241 | "image" 242 | "card"
··· 237 type: "block-type-union"; 238 value: 239 | "datetime" 240 + | "rsvp" 241 | "text" 242 | "image" 243 | "card"
+66 -1
supabase/database.types.ts
··· 323 }, 324 ] 325 } 326 replicache_clients: { 327 Row: { 328 client_group: string ··· 387 } 388 } 389 Enums: { 390 - [_ in never]: never 391 } 392 CompositeTypes: { 393 [_ in never]: never
··· 323 }, 324 ] 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 + } 391 replicache_clients: { 392 Row: { 393 client_group: string ··· 452 } 453 } 454 Enums: { 455 + rsvp_status: "GOING" | "NOT_GOING" | "MAYBE" 456 } 457 CompositeTypes: { 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";