Skip to content

Commit 93a0ef2

Browse files
committed
hotfix: missing waitlist
1 parent 9c72720 commit 93a0ef2

6 files changed

Lines changed: 444 additions & 0 deletions

File tree

src/client/AppgramClient.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ import type {
3333
BlogPostsResponse,
3434
BlogCategory,
3535
BlogFilters,
36+
WaitlistJoinInput,
37+
WaitlistCount,
38+
WaitlistStatus,
39+
WaitlistEntry,
3640
} from '../types'
3741

3842
export interface AppgramClientConfig {
@@ -1134,4 +1138,42 @@ export class AppgramClient {
11341138
project_id: this.projectId,
11351139
})
11361140
}
1141+
1142+
// ============================================================================
1143+
// Waitlist
1144+
// ============================================================================
1145+
1146+
/**
1147+
* Get the total count of users on the waitlist
1148+
*/
1149+
async getWaitlistCount(): Promise<ApiResponse<WaitlistCount>> {
1150+
return this.get<WaitlistCount>(`/api/v1/projects/${this.projectId}/wishlist/count`)
1151+
}
1152+
1153+
/**
1154+
* Join the waitlist
1155+
*/
1156+
async joinWaitlist(data: WaitlistJoinInput): Promise<ApiResponse<WaitlistEntry>> {
1157+
return this.post<WaitlistEntry>(`/api/v1/projects/${this.projectId}/wishlist/join`, data)
1158+
}
1159+
1160+
/**
1161+
* Leave the waitlist
1162+
*/
1163+
async leaveWaitlist(email: string): Promise<ApiResponse<{ success: boolean }>> {
1164+
return this.request<{ success: boolean }>(
1165+
'DELETE',
1166+
`/api/v1/projects/${this.projectId}/wishlist/leave`,
1167+
{ body: { email } }
1168+
)
1169+
}
1170+
1171+
/**
1172+
* Check if an email is on the waitlist
1173+
*/
1174+
async checkWaitlistStatus(email: string): Promise<ApiResponse<WaitlistStatus>> {
1175+
return this.get<WaitlistStatus>(`/api/v1/projects/${this.projectId}/wishlist/status`, {
1176+
email,
1177+
})
1178+
}
11371179
}

src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,4 @@ export {
6666
type UseFeaturedPostsOptions,
6767
type UseFeaturedPostsResult,
6868
} from './useBlog'
69+
export { useWaitlist, type UseWaitlistOptions, type UseWaitlistResult } from './useWaitlist'

src/hooks/useWaitlist.ts

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
/**
2+
* useWaitlist Hook
3+
*
4+
* Manages waitlist operations: join, leave, check status, and get count.
5+
*
6+
* @example
7+
* ```tsx
8+
* import { useWaitlist } from '@appgram/react'
9+
*
10+
* function WaitlistForm() {
11+
* const {
12+
* count,
13+
* isOnWaitlist,
14+
* isLoading,
15+
* isJoining,
16+
* error,
17+
* join,
18+
* leave,
19+
* checkStatus,
20+
* refresh,
21+
* } = useWaitlist({
22+
* email: userEmail, // Optional: auto-check status for this email
23+
* onJoinSuccess: (entry) => console.log('Joined!', entry),
24+
* })
25+
*
26+
* return (
27+
* <div>
28+
* <p>{count} people on the waitlist</p>
29+
* {isOnWaitlist ? (
30+
* <button onClick={() => leave(userEmail)}>Leave Waitlist</button>
31+
* ) : (
32+
* <button onClick={() => join({ email: userEmail })}>Join Waitlist</button>
33+
* )}
34+
* </div>
35+
* )
36+
* }
37+
* ```
38+
*/
39+
40+
import { useState, useEffect, useCallback } from 'react'
41+
import { useAppgramContext } from '../provider/context'
42+
import { getErrorMessage } from '../utils'
43+
import type { WaitlistJoinInput, WaitlistEntry, WaitlistStatus } from '../types'
44+
45+
export interface UseWaitlistOptions {
46+
/**
47+
* Email to auto-check waitlist status for on mount
48+
*/
49+
email?: string
50+
51+
/**
52+
* Whether to fetch the waitlist count on mount
53+
* @default true
54+
*/
55+
fetchCountOnMount?: boolean
56+
57+
/**
58+
* Callback when successfully joined the waitlist
59+
*/
60+
onJoinSuccess?: (entry: WaitlistEntry) => void
61+
62+
/**
63+
* Callback when join fails
64+
*/
65+
onJoinError?: (error: string) => void
66+
67+
/**
68+
* Callback when successfully left the waitlist
69+
*/
70+
onLeaveSuccess?: () => void
71+
72+
/**
73+
* Callback when leave fails
74+
*/
75+
onLeaveError?: (error: string) => void
76+
}
77+
78+
export interface UseWaitlistResult {
79+
/**
80+
* Total count of users on the waitlist
81+
*/
82+
count: number | null
83+
84+
/**
85+
* Whether the provided email is on the waitlist
86+
*/
87+
isOnWaitlist: boolean | null
88+
89+
/**
90+
* Current user's waitlist status details
91+
*/
92+
status: WaitlistStatus | null
93+
94+
/**
95+
* Loading state for initial data fetch
96+
*/
97+
isLoading: boolean
98+
99+
/**
100+
* Loading state for join operation
101+
*/
102+
isJoining: boolean
103+
104+
/**
105+
* Loading state for leave operation
106+
*/
107+
isLeaving: boolean
108+
109+
/**
110+
* Loading state for status check
111+
*/
112+
isCheckingStatus: boolean
113+
114+
/**
115+
* Error message if any operation failed
116+
*/
117+
error: string | null
118+
119+
/**
120+
* Success message after successful operation
121+
*/
122+
successMessage: string | null
123+
124+
/**
125+
* Join the waitlist
126+
*/
127+
join: (data: WaitlistJoinInput) => Promise<WaitlistEntry | null>
128+
129+
/**
130+
* Leave the waitlist
131+
*/
132+
leave: (email: string) => Promise<boolean>
133+
134+
/**
135+
* Check if an email is on the waitlist
136+
*/
137+
checkStatus: (email: string) => Promise<WaitlistStatus | null>
138+
139+
/**
140+
* Refresh the waitlist count
141+
*/
142+
refreshCount: () => Promise<void>
143+
144+
/**
145+
* Clear error and success messages
146+
*/
147+
clearMessages: () => void
148+
}
149+
150+
export function useWaitlist(options: UseWaitlistOptions = {}): UseWaitlistResult {
151+
const { fetchCountOnMount = true } = options
152+
const { client } = useAppgramContext()
153+
154+
const [count, setCount] = useState<number | null>(null)
155+
const [status, setStatus] = useState<WaitlistStatus | null>(null)
156+
const [isLoading, setIsLoading] = useState(false)
157+
const [isJoining, setIsJoining] = useState(false)
158+
const [isLeaving, setIsLeaving] = useState(false)
159+
const [isCheckingStatus, setIsCheckingStatus] = useState(false)
160+
const [error, setError] = useState<string | null>(null)
161+
const [successMessage, setSuccessMessage] = useState<string | null>(null)
162+
163+
const clearMessages = useCallback(() => {
164+
setError(null)
165+
setSuccessMessage(null)
166+
}, [])
167+
168+
const refreshCount = useCallback(async () => {
169+
try {
170+
const response = await client.getWaitlistCount()
171+
if (response.success && response.data) {
172+
setCount(response.data.count)
173+
}
174+
} catch (err) {
175+
console.error('Failed to fetch waitlist count:', err)
176+
}
177+
}, [client])
178+
179+
const checkStatus = useCallback(
180+
async (email: string): Promise<WaitlistStatus | null> => {
181+
setIsCheckingStatus(true)
182+
setError(null)
183+
184+
try {
185+
const response = await client.checkWaitlistStatus(email)
186+
187+
if (response.success && response.data) {
188+
setStatus(response.data)
189+
return response.data
190+
} else {
191+
const errorMsg = getErrorMessage(response.error, 'Failed to check waitlist status')
192+
setError(errorMsg)
193+
return null
194+
}
195+
} catch (err) {
196+
const errorMsg = getErrorMessage(err, 'An error occurred')
197+
setError(errorMsg)
198+
return null
199+
} finally {
200+
setIsCheckingStatus(false)
201+
}
202+
},
203+
[client]
204+
)
205+
206+
const join = useCallback(
207+
async (data: WaitlistJoinInput): Promise<WaitlistEntry | null> => {
208+
setIsJoining(true)
209+
setError(null)
210+
setSuccessMessage(null)
211+
212+
try {
213+
const response = await client.joinWaitlist(data)
214+
215+
if (response.success && response.data) {
216+
setSuccessMessage("You've been added to the waitlist!")
217+
setStatus({ is_on_waitlist: true, position: response.data.position })
218+
options.onJoinSuccess?.(response.data)
219+
// Refresh count after joining
220+
refreshCount()
221+
return response.data
222+
} else {
223+
const errorMsg = getErrorMessage(response.error, 'Failed to join waitlist')
224+
setError(errorMsg)
225+
options.onJoinError?.(errorMsg)
226+
return null
227+
}
228+
} catch (err) {
229+
const errorMsg = getErrorMessage(err, 'An error occurred')
230+
setError(errorMsg)
231+
options.onJoinError?.(errorMsg)
232+
return null
233+
} finally {
234+
setIsJoining(false)
235+
}
236+
},
237+
[client, options, refreshCount]
238+
)
239+
240+
const leave = useCallback(
241+
async (email: string): Promise<boolean> => {
242+
setIsLeaving(true)
243+
setError(null)
244+
setSuccessMessage(null)
245+
246+
try {
247+
const response = await client.leaveWaitlist(email)
248+
249+
if (response.success) {
250+
setSuccessMessage("You've been removed from the waitlist.")
251+
setStatus({ is_on_waitlist: false })
252+
options.onLeaveSuccess?.()
253+
// Refresh count after leaving
254+
refreshCount()
255+
return true
256+
} else {
257+
const errorMsg = getErrorMessage(response.error, 'Failed to leave waitlist')
258+
setError(errorMsg)
259+
options.onLeaveError?.(errorMsg)
260+
return false
261+
}
262+
} catch (err) {
263+
const errorMsg = getErrorMessage(err, 'An error occurred')
264+
setError(errorMsg)
265+
options.onLeaveError?.(errorMsg)
266+
return false
267+
} finally {
268+
setIsLeaving(false)
269+
}
270+
},
271+
[client, options, refreshCount]
272+
)
273+
274+
// Fetch count and optionally check status on mount
275+
useEffect(() => {
276+
const init = async () => {
277+
setIsLoading(true)
278+
279+
if (fetchCountOnMount) {
280+
await refreshCount()
281+
}
282+
283+
if (options.email) {
284+
await checkStatus(options.email)
285+
}
286+
287+
setIsLoading(false)
288+
}
289+
290+
init()
291+
}, [fetchCountOnMount, options.email, refreshCount, checkStatus])
292+
293+
return {
294+
count,
295+
isOnWaitlist: status?.is_on_waitlist ?? null,
296+
status,
297+
isLoading,
298+
isJoining,
299+
isLeaving,
300+
isCheckingStatus,
301+
error,
302+
successMessage,
303+
join,
304+
leave,
305+
checkStatus,
306+
refreshCount,
307+
clearMessages,
308+
}
309+
}

src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export {
4242
useBlogPost,
4343
useBlogCategories,
4444
useFeaturedPosts,
45+
useWaitlist,
4546
type UseWishesOptions,
4647
type UseWishesResult,
4748
type UseWishOptions,
@@ -87,6 +88,8 @@ export {
8788
type UseBlogCategoriesResult,
8889
type UseFeaturedPostsOptions,
8990
type UseFeaturedPostsResult,
91+
type UseWaitlistOptions,
92+
type UseWaitlistResult,
9093
} from './hooks'
9194

9295
// Components
@@ -244,6 +247,11 @@ export type {
244247
BlogCategory,
245248
BlogPostsResponse,
246249
BlogFilters,
250+
// Waitlist types
251+
WaitlistJoinInput,
252+
WaitlistCount,
253+
WaitlistStatus,
254+
WaitlistEntry,
247255
// API types
248256
ApiResponse,
249257
PaginatedResponse,

0 commit comments

Comments
 (0)