diff --git a/app/catalyst-nyc/page.tsx b/app/catalyst-nyc/page.tsx new file mode 100644 index 000000000..c4430d1a5 --- /dev/null +++ b/app/catalyst-nyc/page.tsx @@ -0,0 +1,13 @@ +import type { Metadata } from 'next'; +import { CatalystFlow } from '@/components/events/catalyst/CatalystFlow'; +import { CATALYST_NYC_EVENT } from '@/components/events/catalyst/constants'; + +export const metadata: Metadata = { + title: CATALYST_NYC_EVENT.metadata.title, + description: CATALYST_NYC_EVENT.metadata.description, + robots: { index: false, follow: false, nocache: true }, +}; + +export default function CatalystNycPage() { + return ; +} diff --git a/components/Auth/AuthContent.tsx b/components/Auth/AuthContent.tsx index c60536bd2..e2fcb3a0b 100644 --- a/components/Auth/AuthContent.tsx +++ b/components/Auth/AuthContent.tsx @@ -1,12 +1,14 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, type ReactNode } from 'react'; import SelectProvider from './screens/SelectProvider'; import Login from './screens/Login'; import Signup from './screens/Signup'; import VerifyEmail from './screens/VerifyEmail'; import ForgotPassword from './screens/ForgotPassword'; -import { AuthScreen } from './types'; +import { AuthScreen, type AuthAppearance, type CatalystSurface } from './types'; import AnalyticsService, { LogEvent } from '@/services/analytics.service'; +export type { AuthAppearance, CatalystSurface } from './types'; + interface AuthContentProps { onClose?: () => void; onSuccess?: () => void; @@ -16,6 +18,12 @@ interface AuthContentProps { modalView?: boolean; /** URL to redirect to after Google OAuth login */ callbackUrl?: string; + appearance?: AuthAppearance; + catalystSurface?: CatalystSurface; + onScreenChange?: (screen: AuthScreen) => void; + entryTitle?: ReactNode; + entryNote?: ReactNode; + emailLabel?: string; } export default function AuthContent({ @@ -26,12 +34,22 @@ export default function AuthContent({ showHeader = true, modalView = false, callbackUrl, + appearance = 'default', + catalystSurface = 'dark', + onScreenChange, + entryTitle, + entryNote, + emailLabel, }: AuthContentProps) { const [screen, setScreen] = useState(initialScreen); const [email, setEmail] = useState(''); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(initialError || null); + useEffect(() => { + onScreenChange?.(screen); + }, [screen, onScreenChange]); + useEffect(() => { if (screen === 'LOGIN') { AnalyticsService.logEvent(LogEvent.LOGIN_VIA_EMAIL_INITIATED); @@ -62,6 +80,11 @@ export default function AuthContent({ onContinue={() => setScreen('LOGIN')} onSignup={() => setScreen('SIGNUP')} isLoading={isLoading} + appearance={appearance} + catalystSurface={catalystSurface} + entryTitle={entryTitle} + entryNote={entryNote} + emailLabel={emailLabel} /> )} {screen === 'LOGIN' && ( diff --git a/components/Auth/screens/SelectProvider.tsx b/components/Auth/screens/SelectProvider.tsx index ad2a8c1ec..91493d88d 100644 --- a/components/Auth/screens/SelectProvider.tsx +++ b/components/Auth/screens/SelectProvider.tsx @@ -1,3 +1,4 @@ +import type { ReactNode } from 'react'; import { signIn } from 'next-auth/react'; import { AuthService } from '@/services/auth.service'; import { ApiError } from '@/services/types/api'; @@ -6,6 +7,8 @@ import { useAutoFocus } from '@/hooks/useAutoFocus'; import AnalyticsService, { LogEvent } from '@/services/analytics.service'; import { useReferral } from '@/contexts/ReferralContext'; import { Button } from '@/components/ui/Button'; +import type { AuthAppearance, CatalystSurface } from '../types'; +import { CATALYST_NYC_EVENT } from '@/components/events/catalyst/constants'; interface SelectProviderProps { onContinue: () => void; @@ -19,6 +22,34 @@ interface SelectProviderProps { setIsLoading?: (isLoading: boolean) => void; /** URL to redirect to after Google OAuth login */ callbackUrl?: string; + appearance?: AuthAppearance; + catalystSurface?: CatalystSurface; + entryTitle?: ReactNode; + entryNote?: ReactNode; + emailLabel?: string; +} + +function GoogleGlyph() { + return ( + + + + + + + ); } export default function SelectProvider({ @@ -32,9 +63,18 @@ export default function SelectProvider({ showHeader = true, setIsLoading, callbackUrl, + appearance = 'default', + catalystSurface = 'dark', + entryTitle, + entryNote, + emailLabel, }: SelectProviderProps) { const emailInputRef = useAutoFocus(true); const { referralCode } = useReferral(); + const { auth } = CATALYST_NYC_EVENT; + const isCatalystDark = appearance === 'catalyst' && catalystSurface === 'dark'; + const isCatalystLight = appearance === 'catalyst' && catalystSurface === 'light'; + const resolvedEmailLabel = emailLabel ?? auth.emailLabel; const handleCheckAccount = async (e?: React.FormEvent) => { e?.preventDefault(); @@ -51,7 +91,7 @@ export default function SelectProvider({ if (response.exists) { if (response.auth === 'google') { - signIn('google', { callbackUrl: '/' }); + handleGoogleSignIn(); } else if (response.is_verified) { onContinue(); } else { @@ -68,18 +108,16 @@ export default function SelectProvider({ }; const handleGoogleSignIn = async () => { - AnalyticsService.logEvent(LogEvent.AUTH_VIA_GOOGLE_INITIATED).catch((error) => { - console.error('Analytics failed:', error); + AnalyticsService.logEvent(LogEvent.AUTH_VIA_GOOGLE_INITIATED).catch((analyticsError) => { + console.error('Analytics failed:', analyticsError); }); - // Use prop if provided, otherwise check URL params, fallback to home const searchParams = new URLSearchParams(window.location.search); const originalCallbackUrl = callbackUrl || searchParams.get('callbackUrl') || '/'; let finalCallbackUrl = originalCallbackUrl; if (referralCode) { - // Create referral application URL with referral code and redirect as URL parameters const referralUrl = new URL('/referral/join/apply-referral-code', window.location.origin); referralUrl.searchParams.set('refr', referralCode); referralUrl.searchParams.set('redirect', originalCallbackUrl); @@ -89,9 +127,163 @@ export default function SelectProvider({ signIn('google', { callbackUrl: finalCallbackUrl }); }; + if (isCatalystDark) { + return ( + + {entryTitle} + + {error && {error}} + + + + {resolvedEmailLabel} + + setEmail(e.target.value)} + /> + + {isLoading ? auth.loadingLabel : auth.continueLabel} + + + + + OR + + + + + + {auth.googleLabel} + + + + {entryNote} + + + + ); + } + return ( - {showHeader && ( + {showHeader && appearance === 'default' && ( <> Welcome to ResearchHub{' '} @@ -106,14 +298,25 @@ export default function SelectProvider({ > )} + {isCatalystLight && entryTitle} + {error && {error}} + {isCatalystLight && ( + + {resolvedEmailLabel} + + )} setEmail(e.target.value)} - placeholder="Email" + placeholder={isCatalystLight ? auth.emailPlaceholder : 'Email'} autoCapitalize="none" autoComplete="email" className="w-full p-3 border rounded mb-4" @@ -121,7 +324,7 @@ export default function SelectProvider({ /> - {isLoading ? 'Loading...' : 'Continue'} + {isLoading ? auth.loadingLabel : auth.continueLabel} @@ -156,9 +359,11 @@ export default function SelectProvider({ fill="#EA4335" /> - Continue with Google + {auth.googleLabel} + + {isCatalystLight && entryNote} ); } diff --git a/components/Auth/types.ts b/components/Auth/types.ts index 32b0d5dae..ad8a37659 100644 --- a/components/Auth/types.ts +++ b/components/Auth/types.ts @@ -5,6 +5,9 @@ export type AuthScreen = | 'VERIFY_EMAIL' | 'FORGOT_PASSWORD'; +export type AuthAppearance = 'default' | 'catalyst'; +export type CatalystSurface = 'dark' | 'light'; + export interface BaseScreenProps { onBack?: () => void; onClose: () => void; diff --git a/components/events/catalyst/CatalystArrivalBody.tsx b/components/events/catalyst/CatalystArrivalBody.tsx new file mode 100644 index 000000000..d83cb382c --- /dev/null +++ b/components/events/catalyst/CatalystArrivalBody.tsx @@ -0,0 +1,184 @@ +'use client'; + +import { useEffect, useRef, useState } from 'react'; +import { CATALYST_NYC_EVENT, formatMoney } from './constants'; + +const { + creditAmount, + yieldRate, + yieldLabel, + barCount, + maxYears, + animationIntervalMs, + arrival, + footer, +} = CATALYST_NYC_EVENT; + +const MAX = creditAmount * (1 + yieldRate * maxYears); + +const BARS = Array.from({ length: barCount }, (_, i) => ({ + year: i, + heightPct: Math.max(3, ((creditAmount * (1 + yieldRate * i)) / MAX) * 100), +})); + +interface CatalystArrivalBodyProps { + onClaim: () => void; + compact?: boolean; +} + +export function CatalystArrivalBody({ + onClaim, + compact = false, +}: Readonly) { + const [yr, setYr] = useState(0); + const holdRef = useRef(0); + + useEffect(() => { + const prefersReducedMotion = globalThis.matchMedia('(prefers-reduced-motion: reduce)').matches; + if (prefersReducedMotion) { + return undefined; + } + + const id = setInterval(() => { + setYr((prev) => { + if (prev === 0 && holdRef.current < 3) { + holdRef.current += 1; + return prev; + } + holdRef.current = 0; + return prev >= maxYears ? 0 : prev + 1; + }); + }, animationIntervalMs); + + return () => clearInterval(id); + }, []); + + const value = creditAmount * (1 + yieldRate * yr); + const yearWord = yr === 1 ? 'year' : 'years'; + const phaseLabel = yr === 0 ? 'Your starting balance today' : `After ${yr} ${yearWord}`; + const yrText = `${phaseLabel} ยท ${yieldLabel}`; + + return ( + <> + + {formatMoney(value)} + {yrText} + + {BARS.map(({ year, heightPct }) => { + const active = year <= yr; + return ( + + ); + })} + + + Now + 5 years + 10 years + + + + {arrival.headline} + + + + {arrival.cta} + + {footer} + + + + > + ); +} diff --git a/components/events/catalyst/CatalystArrivalScreen.tsx b/components/events/catalyst/CatalystArrivalScreen.tsx new file mode 100644 index 000000000..bf4366ef0 --- /dev/null +++ b/components/events/catalyst/CatalystArrivalScreen.tsx @@ -0,0 +1,18 @@ +'use client'; + +import { CatalystArrivalBody } from './CatalystArrivalBody'; +import { CatalystLockup } from './CatalystLockup'; +import { CatalystScreenShell } from './CatalystScreenShell'; + +interface CatalystArrivalScreenProps { + onClaim: () => void; +} + +export function CatalystArrivalScreen({ onClaim }: Readonly) { + return ( + + + + + ); +} diff --git a/components/events/catalyst/CatalystAuthEntryChrome.tsx b/components/events/catalyst/CatalystAuthEntryChrome.tsx new file mode 100644 index 000000000..f7d5de840 --- /dev/null +++ b/components/events/catalyst/CatalystAuthEntryChrome.tsx @@ -0,0 +1,106 @@ +'use client'; + +import { CATALYST_NYC_EVENT } from './constants'; + +const { auth } = CATALYST_NYC_EVENT; + +interface CatalystAuthEntryTitleProps { + surface: 'dark' | 'light'; +} + +export function CatalystAuthEntryTitle({ surface }: Readonly) { + const onDark = surface === 'dark'; + + return ( + <> + + {auth.titleLines[0]} + + {auth.titleLines[1]} + + + + > + ); +} + +interface CatalystAuthEntryNoteProps { + surface: 'dark' | 'light'; +} + +function InfoGlyph({ stroke }: Readonly<{ stroke: string }>) { + return ( + + + + + + ); +} + +export function CatalystAuthEntryNote({ surface }: Readonly) { + const onDark = surface === 'dark'; + + return ( + <> + + + + Use the {auth.noteHighlight}. + + + + + > + ); +} diff --git a/components/events/catalyst/CatalystAuthModal.tsx b/components/events/catalyst/CatalystAuthModal.tsx new file mode 100644 index 000000000..fa04ead5e --- /dev/null +++ b/components/events/catalyst/CatalystAuthModal.tsx @@ -0,0 +1,70 @@ +'use client'; + +import AuthContent from '@/components/Auth/AuthContent'; +import { Button } from '@/components/ui/Button'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faXmark } from '@fortawesome/pro-light-svg-icons'; +import { CatalystAuthEntryNote, CatalystAuthEntryTitle } from './CatalystAuthEntryChrome'; +import { CATALYST_NYC_EVENT } from './constants'; +import { CatalystLockup } from './CatalystLockup'; + +const { auth, route } = CATALYST_NYC_EVENT; + +interface CatalystAuthModalProps { + isOpen: boolean; + onClose: () => void; + onSuccess?: () => void; +} + +export function CatalystAuthModal({ + isOpen, + onClose, + onSuccess, +}: Readonly) { + if (!isOpen) { + return null; + } + + return ( + + + + + + + + + + + + } + entryNote={} + /> + + + ); +} diff --git a/components/events/catalyst/CatalystAuthScreen.tsx b/components/events/catalyst/CatalystAuthScreen.tsx new file mode 100644 index 000000000..dd2379d24 --- /dev/null +++ b/components/events/catalyst/CatalystAuthScreen.tsx @@ -0,0 +1,71 @@ +'use client'; + +import { useState } from 'react'; +import AuthContent from '@/components/Auth/AuthContent'; +import type { AuthScreen } from '@/components/Auth/types'; +import { CatalystAuthEntryNote, CatalystAuthEntryTitle } from './CatalystAuthEntryChrome'; +import { CATALYST_NYC_EVENT } from './constants'; +import { CatalystLockup } from './CatalystLockup'; +import { CatalystScreenShell } from './CatalystScreenShell'; + +const { footer, auth, route } = CATALYST_NYC_EVENT; + +interface CatalystAuthScreenProps { + initialScreen?: AuthScreen; +} + +export function CatalystAuthScreen({ + initialScreen = 'SELECT_PROVIDER', +}: Readonly) { + const [screen, setScreen] = useState(initialScreen); + + return ( + + + + + + } + entryNote={} + /> + + + + {footer} + + + + ); +} diff --git a/components/events/catalyst/CatalystDesktopLoggedIn.tsx b/components/events/catalyst/CatalystDesktopLoggedIn.tsx new file mode 100644 index 000000000..f14d06ba2 --- /dev/null +++ b/components/events/catalyst/CatalystDesktopLoggedIn.tsx @@ -0,0 +1,32 @@ +'use client'; + +import { PageLayout } from '@/app/layouts/PageLayout'; +import { CatalystLockup } from './CatalystLockup'; +import { CatalystLoggedInBody } from './CatalystLoggedInBody'; + +interface CatalystDesktopLoggedInProps { + email: string; +} + +export function CatalystDesktopLoggedIn({ email }: Readonly) { + return ( + + + + + + + + + + + ); +} diff --git a/components/events/catalyst/CatalystDesktopOffer.tsx b/components/events/catalyst/CatalystDesktopOffer.tsx new file mode 100644 index 000000000..2479a3706 --- /dev/null +++ b/components/events/catalyst/CatalystDesktopOffer.tsx @@ -0,0 +1,41 @@ +'use client'; + +import { PageLayout } from '@/app/layouts/PageLayout'; +import { CatalystArrivalBody } from './CatalystArrivalBody'; +import { CatalystLockup } from './CatalystLockup'; +import { CatalystVioletBackdrop } from './CatalystVioletBackdrop'; + +interface CatalystDesktopOfferProps { + onClaim: () => void; +} + +export function CatalystDesktopOffer({ onClaim }: Readonly) { + return ( + + + + + + + + + + + + + + ); +} diff --git a/components/events/catalyst/CatalystFlow.tsx b/components/events/catalyst/CatalystFlow.tsx new file mode 100644 index 000000000..5178fb73e --- /dev/null +++ b/components/events/catalyst/CatalystFlow.tsx @@ -0,0 +1,100 @@ +'use client'; + +import { useState } from 'react'; +import { PageLayout } from '@/app/layouts/PageLayout'; +import { useUser } from '@/contexts/UserContext'; +import { CatalystArrivalScreen } from './CatalystArrivalScreen'; +import { CatalystAuthModal } from './CatalystAuthModal'; +import { CatalystAuthScreen } from './CatalystAuthScreen'; +import { CatalystDesktopLoggedIn } from './CatalystDesktopLoggedIn'; +import { CatalystDesktopOffer } from './CatalystDesktopOffer'; +import { CatalystLoggedInScreen } from './CatalystLoggedInScreen'; +import { CatalystLockup } from './CatalystLockup'; +import { CatalystScreenShell } from './CatalystScreenShell'; +import { useCatalystLayout } from './useCatalystLayout'; + +type Step = 'arrival' | 'auth'; + +function MobileLoadingState() { + return ( + + + + + + ); +} + +function DesktopLoadingState() { + return ( + + + + + + ); +} + +export function CatalystFlow() { + const { user, isLoading } = useUser(); + const isMobile = useCatalystLayout(); + const [step, setStep] = useState('arrival'); + const [authOpen, setAuthOpen] = useState(false); + + if (isLoading) { + return isMobile ? : ; + } + + if (user?.email) { + return isMobile ? ( + + ) : ( + + ); + } + + if (isMobile) { + if (step === 'auth') { + return ; + } + return setStep('auth')} />; + } + + return ( + <> + setAuthOpen(true)} /> + setAuthOpen(false)} + onSuccess={() => setAuthOpen(false)} + /> + > + ); +} diff --git a/components/events/catalyst/CatalystLockup.tsx b/components/events/catalyst/CatalystLockup.tsx new file mode 100644 index 000000000..fe744c9ea --- /dev/null +++ b/components/events/catalyst/CatalystLockup.tsx @@ -0,0 +1,64 @@ +'use client'; + +import { Logo } from '@/components/ui/Logo'; +import { CatalystNyc } from './CatalystNyc'; + +const LOGO_SIZE = 25; +const NYC_HEIGHT = 15; +const GAP = 13; +const DIV_HEIGHT = 22; +const CAT_FONT = 21; +const CAT_GAP = 9; + +interface CatalystLockupProps { + theme?: 'onDark' | 'onLight'; +} + +export function CatalystLockup({ theme = 'onDark' }: Readonly) { + const onDark = theme === 'onDark'; + + return ( + + + + + Catalyst + + + + + + ); +} diff --git a/components/events/catalyst/CatalystLoggedInBody.tsx b/components/events/catalyst/CatalystLoggedInBody.tsx new file mode 100644 index 000000000..3b8a9a00a --- /dev/null +++ b/components/events/catalyst/CatalystLoggedInBody.tsx @@ -0,0 +1,236 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { signOut } from 'next-auth/react'; +import AnalyticsService from '@/services/analytics.service'; +import { AuthSharingService } from '@/services/auth-sharing.service'; +import { CATALYST_NYC_EVENT, formatCredit } from './constants'; + +const { footer, loggedIn, creditAmount, route } = CATALYST_NYC_EVENT; + +interface CatalystLoggedInBodyProps { + email: string; + variant: 'mobile' | 'desktop'; + showFooter?: boolean; +} + +export function CatalystLoggedInBody({ + email, + variant, + showFooter = false, +}: Readonly) { + const router = useRouter(); + const isMobile = variant === 'mobile'; + + // Sign out across both RH apps, then return here so the attendee can re-auth with the correct email. + const handleSignOut = () => { + AuthSharingService.removeSharedAuthToken(); + AnalyticsService.clearUserSession(); + signOut({ callbackUrl: route }); + }; + + return ( + + + + + + + + + Your {formatCredit(creditAmount)} {loggedIn.titleSuffix} + + + {loggedIn.reviewNote} + + {loggedIn.eyebrow} + {email} + {loggedIn.emailNote} + + + router.push('/')}> + {loggedIn.continueLabel} + + + {loggedIn.signOutLabel} + + + + {showFooter && {footer}} + + + + ); +} diff --git a/components/events/catalyst/CatalystLoggedInScreen.tsx b/components/events/catalyst/CatalystLoggedInScreen.tsx new file mode 100644 index 000000000..5d140f23d --- /dev/null +++ b/components/events/catalyst/CatalystLoggedInScreen.tsx @@ -0,0 +1,40 @@ +'use client'; + +import { CATALYST_NYC_EVENT } from './constants'; +import { CatalystLockup } from './CatalystLockup'; +import { CatalystLoggedInBody } from './CatalystLoggedInBody'; +import { CatalystScreenShell } from './CatalystScreenShell'; + +const { footer } = CATALYST_NYC_EVENT; + +interface CatalystLoggedInScreenProps { + email: string; +} + +export function CatalystLoggedInScreen({ email }: Readonly) { + return ( + + + + + + + + {footer} + + + + ); +} diff --git a/components/events/catalyst/CatalystNyc.tsx b/components/events/catalyst/CatalystNyc.tsx new file mode 100644 index 000000000..300ed1949 --- /dev/null +++ b/components/events/catalyst/CatalystNyc.tsx @@ -0,0 +1,76 @@ +const NYC_CELLS: ReadonlyArray = [ + [0, 0], + [12.69, 0], + [0, 3.17], + [3.17, 3.17], + [12.69, 3.17], + [0, 6.34], + [3.17, 6.34], + [12.69, 6.34], + [0, 9.52], + [6.34, 9.52], + [12.69, 9.52], + [0, 12.69], + [9.52, 12.69], + [12.69, 12.69], + [0, 15.86], + [9.52, 15.86], + [12.69, 15.86], + [0, 19.03], + [12.69, 19.03], + [18.89, 0], + [31.58, 0], + [18.89, 3.17], + [31.58, 3.17], + [22.06, 6.34], + [28.4, 6.34], + [25.23, 9.52], + [25.23, 12.69], + [25.23, 15.86], + [25.23, 19.03], + [40.95, 0], + [44.12, 0], + [47.29, 0], + [37.78, 3.17], + [50.46, 3.17], + [37.78, 6.34], + [37.78, 9.52], + [37.78, 12.69], + [37.78, 15.86], + [50.46, 15.86], + [40.95, 19.03], + [44.12, 19.03], + [47.29, 19.03], +]; + +const NATIVE_WIDTH = 53.1; +const NATIVE_HEIGHT = 21.6; + +interface CatalystNycProps { + fill: string; + height?: number; + className?: string; +} + +export function CatalystNyc({ + fill, + height = NATIVE_HEIGHT, + className, +}: Readonly) { + const width = (height * NATIVE_WIDTH) / NATIVE_HEIGHT; + return ( + + {NYC_CELLS.map(([x, y]) => ( + + ))} + + ); +} diff --git a/components/events/catalyst/CatalystScreenShell.tsx b/components/events/catalyst/CatalystScreenShell.tsx new file mode 100644 index 000000000..260f81bea --- /dev/null +++ b/components/events/catalyst/CatalystScreenShell.tsx @@ -0,0 +1,64 @@ +'use client'; + +import type { ReactNode } from 'react'; +import { CatalystVioletBackdrop } from './CatalystVioletBackdrop'; + +interface CatalystScreenShellProps { + children: ReactNode; + contentLayout?: 'spread' | 'stack'; +} + +export function CatalystScreenShell({ + children, + contentLayout = 'stack', +}: Readonly) { + return ( + + + + + {children} + + + + + + ); +} diff --git a/components/events/catalyst/CatalystVioletBackdrop.tsx b/components/events/catalyst/CatalystVioletBackdrop.tsx new file mode 100644 index 000000000..801fe57f9 --- /dev/null +++ b/components/events/catalyst/CatalystVioletBackdrop.tsx @@ -0,0 +1,35 @@ +'use client'; + +export function CatalystVioletBackdrop() { + return ( + <> + + + + + > + ); +} diff --git a/components/events/catalyst/constants.ts b/components/events/catalyst/constants.ts new file mode 100644 index 000000000..cb83a05fe --- /dev/null +++ b/components/events/catalyst/constants.ts @@ -0,0 +1,46 @@ +export const CATALYST_NYC_EVENT = { + route: '/catalyst-nyc', + eventName: 'Catalyst NYC', + footer: 'Catalyst NYC promotional event', + creditAmount: 500, + yieldRate: 0.561, + yieldLabel: '56.1% / yr', + barCount: 11, + maxYears: 10, + animationIntervalMs: 900, + arrival: { + headline: "We want you to fund science โ here's some Funding Credits to get started.", + cta: 'Claim my $500', + }, + auth: { + titleLines: ['Sign-up to', 'claim your $500'] as const, + emailLabel: 'Your email', + emailPlaceholder: 'name@institution.com', + continueLabel: 'Continue', + loadingLabel: 'Loading...', + googleLabel: 'Continue with Google', + noteHighlight: 'same email you registered with', + }, + loggedIn: { + titleSuffix: 'is on the way', + reviewNote: + 'Our team manually reviews each request and will credit your account within 48 hours.', + eyebrow: 'Signed in as', + emailNote: "Make sure this is the email you registered with โ it's the account we'll credit.", + continueLabel: 'Explore ResearchHub', + signOutLabel: 'Not your email? Sign out', + }, + metadata: { + title: 'Catalyst NYC โ Join ResearchHub', + description: + 'Catalyst NYC attendees: claim $500 in Funding Credits to fund science. Sign up with the email you registered with.', + }, +} as const; + +export function formatCredit(amount: number): string { + return `$${amount.toLocaleString('en-US')}`; +} + +export function formatMoney(n: number): string { + return `$${Math.round(n).toLocaleString('en-US')}`; +} diff --git a/components/events/catalyst/useCatalystLayout.ts b/components/events/catalyst/useCatalystLayout.ts new file mode 100644 index 000000000..ce98aa353 --- /dev/null +++ b/components/events/catalyst/useCatalystLayout.ts @@ -0,0 +1,25 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +const MOBILE_BREAKPOINT = 640; + +/** + * Mobile-first layout for the Catalyst QR route. Defaults to mobile until the + * viewport is measured so phone users never flash the desktop PageLayout. + */ +export function useCatalystLayout(): boolean { + const [isMobile, setIsMobile] = useState(true); + + useEffect(() => { + const checkMobile = () => { + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + }; + + checkMobile(); + window.addEventListener('resize', checkMobile); + return () => window.removeEventListener('resize', checkMobile); + }, []); + + return isMobile; +} diff --git a/components/modals/SignupModalContainer.tsx b/components/modals/SignupModalContainer.tsx index b56d9f869..7a452698c 100644 --- a/components/modals/SignupModalContainer.tsx +++ b/components/modals/SignupModalContainer.tsx @@ -10,6 +10,7 @@ const EXCLUDED_PATHS = [ '/', // landing page '/referral/join', // referral join page '/referral/join/apply-referral-code', // referral apply referral code page + '/catalyst-nyc', // add any other paths where we don't want the modal to show ];
{error}
+ + + Use the {auth.noteHighlight}. + +
{loggedIn.reviewNote}
{loggedIn.eyebrow}
{email}
{loggedIn.emailNote}
{footer}