1+ "use client" ;
2+
3+ import { useState , useEffect , useRef } from "react" ;
4+ import { CheckIcon } from "@heroicons/react/24/outline" ;
5+ import { Button } from "@/components/ui/button" ;
6+
7+ interface CreditsWaitlistFormProps {
8+ variant ?: "inline" | "card" ;
9+ className ?: string ;
10+ initialCount ?: number | null ;
11+ }
12+
13+ export function CreditsWaitlistForm ( {
14+ variant = "card" ,
15+ className = "" ,
16+ initialCount,
17+ } : CreditsWaitlistFormProps ) {
18+ const [ email , setEmail ] = useState ( "" ) ;
19+ const [ isLoading , setIsLoading ] = useState ( false ) ;
20+ const [ isSuccess , setIsSuccess ] = useState ( false ) ;
21+ const [ error , setError ] = useState < string | null > ( null ) ;
22+ const [ position , setPosition ] = useState < number | null > ( null ) ;
23+ const [ hasSharedTwitter , setHasSharedTwitter ] = useState ( false ) ;
24+ const [ hasSharedLinkedIn , setHasSharedLinkedIn ] = useState ( false ) ;
25+ const [ pendingShareTwitter , setPendingShareTwitter ] = useState ( false ) ;
26+ const [ pendingShareLinkedIn , setPendingShareLinkedIn ] = useState ( false ) ;
27+ const [ isReturningUser , setIsReturningUser ] = useState ( false ) ;
28+
29+ // Ref to store interval ID for cleanup
30+ const windowCheckInterval = useRef < NodeJS . Timeout > ( ) ;
31+
32+ // Load email from localStorage on mount
33+ useEffect ( ( ) => {
34+ const savedEmail = localStorage . getItem ( 'waitlist_email' ) ;
35+ if ( savedEmail ) {
36+ setEmail ( savedEmail ) ;
37+ }
38+ } , [ ] ) ;
39+
40+ const apiUrl = process . env . NEXT_PUBLIC_API_URL || "https://api.helicone.ai" ;
41+
42+ // Cleanup interval on unmount
43+ useEffect ( ( ) => {
44+ return ( ) => {
45+ if ( windowCheckInterval . current ) {
46+ clearInterval ( windowCheckInterval . current ) ;
47+ }
48+ } ;
49+ } , [ ] ) ;
50+
51+ const handleSubmit = async ( e : React . FormEvent ) => {
52+ e . preventDefault ( ) ;
53+ setError ( null ) ;
54+
55+ if ( ! email ) {
56+ setError ( "Please enter your email address" ) ;
57+ return ;
58+ }
59+
60+ const emailRegex = / ^ [ ^ \s @ ] + @ [ ^ \s @ ] + \. [ ^ \s @ ] + $ / ;
61+ if ( ! emailRegex . test ( email ) ) {
62+ setError ( "Please enter a valid email address" ) ;
63+ return ;
64+ }
65+
66+ setIsLoading ( true ) ;
67+
68+ try {
69+ const response = await fetch (
70+ `${ apiUrl } /v1/public/waitlist/feature` ,
71+ {
72+ method : "POST" ,
73+ headers : {
74+ "Content-Type" : "application/json" ,
75+ Authorization : "Bearer undefined" ,
76+ } ,
77+ body : JSON . stringify ( {
78+ email,
79+ feature : "credits" ,
80+ } ) ,
81+ }
82+ ) ;
83+
84+ const result = await response . json ( ) ;
85+
86+ if ( ! response . ok ) {
87+ if ( result . error === "already_on_waitlist" ) {
88+ setError ( "You're already on the waitlist!" ) ;
89+ } else {
90+ setError ( "Failed to join waitlist. Please try again." ) ;
91+ }
92+ } else {
93+ // Save email to localStorage on successful submission
94+ localStorage . setItem ( 'waitlist_email' , email ) ;
95+
96+ // Check if user was already on the list
97+ if ( result . data ?. alreadyOnList ) {
98+ setIsReturningUser ( true ) ;
99+ setPosition ( result . data . position ) ;
100+ // Set which platforms they've already shared
101+ if ( result . data . sharedPlatforms ?. includes ( "twitter" ) ) {
102+ setHasSharedTwitter ( true ) ;
103+ }
104+ if ( result . data . sharedPlatforms ?. includes ( "linkedin" ) ) {
105+ setHasSharedLinkedIn ( true ) ;
106+ }
107+ setIsSuccess ( true ) ;
108+ } else {
109+ // New user added to waitlist
110+ if ( result . data ?. position ) {
111+ setPosition ( result . data . position ) ;
112+ }
113+ setIsSuccess ( true ) ;
114+ }
115+ }
116+ } catch ( err ) {
117+ console . error ( "Error joining waitlist:" , err ) ;
118+ setError ( "Something went wrong. Please try again." ) ;
119+ } finally {
120+ setIsLoading ( false ) ;
121+ }
122+ } ;
123+
124+ const openShareWindow = ( platform : "twitter" | "linkedin" ) => {
125+ // Don't allow multiple shares on same platform
126+ if ( ( platform === "twitter" && ( hasSharedTwitter || pendingShareTwitter ) ) ||
127+ ( platform === "linkedin" && ( hasSharedLinkedIn || pendingShareLinkedIn ) ) ) {
128+ return ;
129+ }
130+
131+ // Clear any existing interval
132+ if ( windowCheckInterval . current ) {
133+ clearInterval ( windowCheckInterval . current ) ;
134+ windowCheckInterval . current = undefined ;
135+ }
136+
137+ // Open the share link in a popup
138+ let shareWindow ;
139+ if ( platform === "twitter" ) {
140+ const tweetUrl = `https://x.com/coleywoleyyy/status/1965525511071039632` ;
141+ shareWindow = window . open ( tweetUrl , "twitter-share" , "width=600,height=700,left=200,top=100" ) ;
142+ setPendingShareTwitter ( true ) ;
143+ } else {
144+ // For LinkedIn, open in a popup
145+ const linkedinUrl = `https://www.linkedin.com/posts/colegottdank_the-helicone-yc-w23-team-goes-to-topgolf-activity-7365872991773069312-7koL` ;
146+ shareWindow = window . open ( linkedinUrl , "linkedin-share" , "width=700,height=700,left=200,top=100" ) ;
147+ setPendingShareLinkedIn ( true ) ;
148+ }
149+
150+ // Check when window closes
151+ if ( shareWindow ) {
152+ const checkClosed = setInterval ( ( ) => {
153+ if ( shareWindow . closed ) {
154+ clearInterval ( checkClosed ) ;
155+ windowCheckInterval . current = undefined ;
156+ // Window closed, keep pending state to show confirmation
157+ }
158+ } , 500 ) ;
159+
160+ // Store the interval ID for cleanup
161+ windowCheckInterval . current = checkClosed ;
162+ }
163+ } ;
164+
165+ const confirmShare = async ( platform : "twitter" | "linkedin" ) => {
166+ // Mark as shared
167+ if ( platform === "twitter" ) {
168+ setHasSharedTwitter ( true ) ;
169+ setPendingShareTwitter ( false ) ;
170+ } else {
171+ setHasSharedLinkedIn ( true ) ;
172+ setPendingShareLinkedIn ( false ) ;
173+ }
174+
175+ // Track the share (give them 10 points)
176+ try {
177+ const response = await fetch (
178+ `${ apiUrl } /v1/public/waitlist/feature/share` ,
179+ {
180+ method : "POST" ,
181+ headers : {
182+ "Content-Type" : "application/json" ,
183+ Authorization : "Bearer undefined" ,
184+ } ,
185+ body : JSON . stringify ( {
186+ email,
187+ feature : "credits" ,
188+ platform,
189+ } ) ,
190+ }
191+ ) ;
192+
193+ if ( response . ok ) {
194+ const result = await response . json ( ) ;
195+ if ( result . data ?. newPosition ) {
196+ setPosition ( result . data . newPosition ) ;
197+ }
198+ }
199+ } catch ( err ) {
200+ console . error ( "Error tracking share:" , err ) ;
201+ }
202+ } ;
203+
204+ const cancelShare = ( platform : "twitter" | "linkedin" ) => {
205+ if ( platform === "twitter" ) {
206+ setPendingShareTwitter ( false ) ;
207+ } else {
208+ setPendingShareLinkedIn ( false ) ;
209+ }
210+ } ;
211+
212+ if ( isSuccess ) {
213+ return (
214+ < div className = "flex flex-col items-center gap-2" >
215+ { /* Success message */ }
216+ < div className = "flex items-center justify-center gap-2" >
217+ < CheckIcon className = "h-5 w-5 text-brand flex-shrink-0" />
218+ < p className = "text-sm text-slate-700" >
219+ < span className = "font-semibold" >
220+ { isReturningUser
221+ ? `You're already on the waitlist! You're #${ position ?. toLocaleString ( ) } in line`
222+ : `Success! You're #${ position ?. toLocaleString ( ) } in line` }
223+ </ span >
224+ </ p >
225+ </ div >
226+ { /* Share section */ }
227+ { ( ! hasSharedTwitter || ! hasSharedLinkedIn ) ? (
228+ < div className = "flex flex-col items-center gap-2" >
229+ < p className = "text-sm text-slate-600" >
230+ { isReturningUser && ( hasSharedTwitter || hasSharedLinkedIn )
231+ ? "Share on more platforms to move up faster"
232+ : "Share on social to move up the waitlist faster" }
233+ </ p >
234+ < div className = "flex gap-2" >
235+ < button
236+ onClick = { ( ) => openShareWindow ( "twitter" ) }
237+ disabled = { hasSharedTwitter || pendingShareTwitter }
238+ className = { `px-4 h-[42px] rounded-lg text-sm font-medium transition-colors ${
239+ hasSharedTwitter
240+ ? "bg-gray-200 text-gray-500 cursor-not-allowed"
241+ : pendingShareTwitter
242+ ? "bg-gray-100 text-gray-600"
243+ : "bg-black text-white hover:bg-gray-800"
244+ } `}
245+ >
246+ { hasSharedTwitter ? "✓ X" :
247+ pendingShareTwitter ? "..." :
248+ "Share X" }
249+ </ button >
250+ < button
251+ onClick = { ( ) => openShareWindow ( "linkedin" ) }
252+ disabled = { hasSharedLinkedIn || pendingShareLinkedIn }
253+ className = { `px-4 h-[42px] rounded-lg text-sm font-medium transition-colors ${
254+ hasSharedLinkedIn
255+ ? "bg-gray-200 text-gray-500 cursor-not-allowed"
256+ : pendingShareLinkedIn
257+ ? "bg-blue-100 text-blue-600"
258+ : "bg-blue-600 text-white hover:bg-blue-700"
259+ } `}
260+ >
261+ { hasSharedLinkedIn ? "✓ LinkedIn" :
262+ pendingShareLinkedIn ? "..." :
263+ "Share LinkedIn" }
264+ </ button >
265+ </ div >
266+ </ div >
267+ ) : (
268+ // Both platforms shared
269+ hasSharedTwitter && hasSharedLinkedIn && (
270+ < p className = "text-sm text-slate-600" >
271+ Thanks for sharing! You've maximized your position boost.
272+ </ p >
273+ )
274+ ) }
275+
276+ { /* Compact confirmation UI */ }
277+ { ( pendingShareTwitter || pendingShareLinkedIn ) && (
278+ < div className = "flex items-center gap-3 p-2 bg-amber-50 border border-amber-200 rounded-lg" >
279+ < p className = "text-sm text-amber-900" >
280+ Did you share?
281+ </ p >
282+ < button
283+ onClick = { ( ) => confirmShare ( pendingShareTwitter ? "twitter" : "linkedin" ) }
284+ className = "px-3 py-1 bg-green-600 text-white rounded text-xs font-medium hover:bg-green-700"
285+ >
286+ Yes ✓
287+ </ button >
288+ < button
289+ onClick = { ( ) => cancelShare ( pendingShareTwitter ? "twitter" : "linkedin" ) }
290+ className = "px-3 py-1 bg-white border border-slate-300 text-slate-600 rounded text-xs hover:bg-slate-50"
291+ >
292+ Not yet
293+ </ button >
294+ </ div >
295+ ) }
296+ </ div >
297+ ) ;
298+ }
299+
300+ if ( variant === "card" ) {
301+ return (
302+ < div className = { `rounded-xl border border-slate-200 bg-white p-6 shadow-sm text-center ${ className } ` } >
303+ < h3 className = "mb-2 text-lg font-semibold text-slate-900" >
304+ Join the Waitlist
305+ </ h3 >
306+ < p className = "mb-4 text-sm text-slate-600" >
307+ { initialCount && initialCount > 0 ? (
308+ < >
309+ < span className = "font-semibold text-brand" > { initialCount . toLocaleString ( ) } + people</ span > are already waiting.
310+ < br />
311+ Be next in line for beta access.
312+ </ >
313+ ) : (
314+ "Be the first to know when Credits launches."
315+ ) }
316+ </ p >
317+ < form onSubmit = { handleSubmit } className = "flex flex-col gap-3" >
318+ < input
319+ type = "email"
320+ placeholder = "Enter your email"
321+ value = { email }
322+ onChange = { ( e ) => setEmail ( e . target . value ) }
323+ className = "rounded-lg border border-slate-300 px-4 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
324+ disabled = { isLoading }
325+ />
326+ { error && < p className = "text-sm text-red-600" > { error } </ p > }
327+ < Button
328+ type = "submit"
329+ disabled = { isLoading }
330+ className = "w-full"
331+ variant = "default"
332+ >
333+ { isLoading ? "Joining..." : "Join Waitlist" }
334+ </ Button >
335+ </ form >
336+ </ div >
337+ ) ;
338+ }
339+
340+ // Inline variant - Compact horizontal layout
341+ return (
342+ < div className = { `${ className } ` } >
343+ < div className = "flex flex-col items-center gap-2" >
344+ < p className = "text-sm text-slate-600 h-5" >
345+ { initialCount && initialCount > 0 ? (
346+ < >
347+ < span className = "font-semibold text-brand" > { initialCount . toLocaleString ( ) } + people</ span > already waiting
348+ </ >
349+ ) : (
350+ < span > </ span >
351+ ) }
352+ </ p >
353+ < form
354+ onSubmit = { handleSubmit }
355+ className = "flex flex-col sm:flex-row gap-2 w-full max-w-md mx-auto"
356+ >
357+ < input
358+ type = "email"
359+ placeholder = "Enter your email"
360+ value = { email }
361+ onChange = { ( e ) => setEmail ( e . target . value ) }
362+ className = "flex-1 rounded-lg border border-slate-300 px-4 py-2.5 text-sm focus:border-brand focus:outline-none focus:ring-1 focus:ring-brand"
363+ disabled = { isLoading }
364+ />
365+ < Button
366+ type = "submit"
367+ disabled = { isLoading }
368+ variant = "default"
369+ className = "px-6 py-2.5 h-auto"
370+ >
371+ { isLoading ? "Joining..." : "Join Waitlist" }
372+ </ Button >
373+ </ form >
374+ { error && (
375+ < p className = "text-sm text-red-600" >
376+ { error }
377+ </ p >
378+ ) }
379+ </ div >
380+ </ div >
381+ ) ;
382+ }
0 commit comments