@growsober/sdk 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/api/mutations/admin.d.ts +1 -20
  2. package/dist/api/mutations/admin.js +1 -1
  3. package/dist/api/mutations/auth.d.ts +83 -1
  4. package/dist/api/mutations/auth.js +102 -1
  5. package/dist/api/mutations/event-chat.d.ts +84 -7
  6. package/dist/api/mutations/event-chat.js +1 -1
  7. package/dist/api/mutations/matching.d.ts +2 -19
  8. package/dist/api/mutations/matching.js +6 -6
  9. package/dist/api/mutations/support.d.ts +5 -5
  10. package/dist/api/mutations/support.js +1 -1
  11. package/dist/api/queries/ambassadors.d.ts +71 -5
  12. package/dist/api/queries/ambassadors.js +1 -1
  13. package/dist/api/queries/cities.d.ts +52 -0
  14. package/dist/api/queries/cities.js +82 -0
  15. package/dist/api/queries/event-chat.d.ts +30 -4
  16. package/dist/api/queries/grow90.d.ts +46 -4
  17. package/dist/api/queries/hubs.d.ts +2 -2
  18. package/dist/api/queries/hubs.js +1 -1
  19. package/dist/api/queries/index.d.ts +1 -0
  20. package/dist/api/queries/index.js +2 -1
  21. package/dist/api/queries/map.d.ts +3 -3
  22. package/dist/api/queries/map.js +1 -1
  23. package/dist/api/queries/support.d.ts +88 -8
  24. package/dist/api/queries/support.js +1 -1
  25. package/dist/api/types.d.ts +63 -393
  26. package/dist/api/types.js +4 -4
  27. package/package.json +1 -1
  28. package/src/api/mutations/admin.ts +1 -20
  29. package/src/api/mutations/auth.ts +114 -0
  30. package/src/api/mutations/event-chat.ts +7 -7
  31. package/src/api/mutations/matching.ts +12 -32
  32. package/src/api/mutations/support.ts +11 -11
  33. package/src/api/queries/ambassadors.ts +6 -3
  34. package/src/api/queries/cities.ts +194 -0
  35. package/src/api/queries/hubs.ts +3 -3
  36. package/src/api/queries/index.ts +1 -0
  37. package/src/api/queries/map.ts +10 -10
  38. package/src/api/queries/support.ts +8 -8
  39. package/src/api/types.ts +96 -448
@@ -15,6 +15,7 @@ import {
15
15
  useQueryClient,
16
16
  } from '@tanstack/react-query';
17
17
  import { getApiClient } from '../client';
18
+ import type { AdminUpdateUserRequest, AdminCreateContentRequest } from '../types';
18
19
  import { adminKeys } from '../queries/admin';
19
20
  import type {
20
21
  UserResponse,
@@ -31,27 +32,7 @@ import type {
31
32
  // REQUEST TYPES
32
33
  // ============================================================================
33
34
 
34
- export interface AdminUpdateUserRequest {
35
- displayName?: string;
36
- email?: string;
37
- role?: string;
38
- isPremium?: boolean;
39
- isSuspended?: boolean;
40
- }
41
35
 
42
- export interface AdminCreateContentRequest {
43
- title: string;
44
- type: string;
45
- category: string;
46
- description?: string;
47
- content?: string;
48
- mediaUrl?: string;
49
- thumbnailUrl?: string;
50
- duration?: number;
51
- isPremium?: boolean;
52
- isFeatured?: boolean;
53
- sortOrder?: number;
54
- }
55
36
 
56
37
  export interface AdminUpdateContentRequest extends Partial<AdminCreateContentRequest> {}
57
38
 
@@ -17,6 +17,9 @@ import type {
17
17
  FirebaseAuthRequest,
18
18
  AuthResponse,
19
19
  TokenResponse,
20
+ SendOtpRequest,
21
+ OtpSentResponse,
22
+ VerifyOtpRequest,
20
23
  } from '../types';
21
24
 
22
25
  // ============================================================================
@@ -348,3 +351,114 @@ export function useFirebaseAuth(
348
351
  ...options,
349
352
  });
350
353
  }
354
+
355
+ /**
356
+ * Response type for verify OTP including isNewUser flag
357
+ */
358
+ export type VerifyOtpResponse = AuthResponse & { isNewUser: boolean };
359
+
360
+ /**
361
+ * Send OTP to phone number
362
+ *
363
+ * @description
364
+ * Sends a verification code to the specified phone number via SMS or voice call.
365
+ * Used for passwordless authentication.
366
+ *
367
+ * @endpoint POST /auth/phone/send-otp
368
+ *
369
+ * @example
370
+ * ```tsx
371
+ * import { useSendOtp } from '@growsober/sdk';
372
+ *
373
+ * function PhoneInputScreen() {
374
+ * const { mutate: sendOtp, isPending, error } = useSendOtp({
375
+ * onSuccess: (data) => {
376
+ * console.log('OTP sent to:', data.phone);
377
+ * navigation.navigate('VerifyOtp', { phone });
378
+ * },
379
+ * onError: (error) => {
380
+ * Alert.alert('Error', error.message);
381
+ * },
382
+ * });
383
+ *
384
+ * const handleSend = () => {
385
+ * sendOtp({ phone: '+1234567890', channel: 'sms' });
386
+ * };
387
+ *
388
+ * return <Button onPress={handleSend} disabled={isPending} />;
389
+ * }
390
+ * ```
391
+ *
392
+ * @param options - TanStack Query mutation options
393
+ * @returns TanStack Query mutation result
394
+ */
395
+ export function useSendOtp(
396
+ options?: Omit<UseMutationOptions<OtpSentResponse, Error, SendOtpRequest>, 'mutationFn'>
397
+ ): UseMutationResult<OtpSentResponse, Error, SendOtpRequest> {
398
+ return useMutation({
399
+ mutationFn: async (data: SendOtpRequest): Promise<OtpSentResponse> => {
400
+ const client = getApiClient();
401
+ const response = await client.post<OtpSentResponse>('/auth/phone/send-otp', data);
402
+ return response.data;
403
+ },
404
+ ...options,
405
+ });
406
+ }
407
+
408
+ /**
409
+ * Verify OTP and authenticate
410
+ *
411
+ * @description
412
+ * Verifies the OTP code sent to the phone number and authenticates the user.
413
+ * If the user doesn't exist, a new account is created automatically.
414
+ * Returns authentication tokens and user information.
415
+ *
416
+ * @endpoint POST /auth/phone/verify-otp
417
+ *
418
+ * @example
419
+ * ```tsx
420
+ * import { useVerifyOtp } from '@growsober/sdk';
421
+ *
422
+ * function VerifyOtpScreen({ phone }) {
423
+ * const { mutate: verifyOtp, isPending, error } = useVerifyOtp({
424
+ * onSuccess: async (data) => {
425
+ * await SecureStore.setItemAsync('accessToken', data.accessToken);
426
+ * await SecureStore.setItemAsync('refreshToken', data.refreshToken);
427
+ *
428
+ * if (data.isNewUser) {
429
+ * navigation.navigate('Onboarding');
430
+ * } else {
431
+ * navigation.navigate('Home');
432
+ * }
433
+ * },
434
+ * });
435
+ *
436
+ * const handleVerify = (code: string) => {
437
+ * verifyOtp({ phone, code });
438
+ * };
439
+ *
440
+ * return <OtpInput onComplete={handleVerify} disabled={isPending} />;
441
+ * }
442
+ * ```
443
+ *
444
+ * @param options - TanStack Query mutation options
445
+ * @returns TanStack Query mutation result
446
+ */
447
+ export function useVerifyOtp(
448
+ options?: Omit<UseMutationOptions<VerifyOtpResponse, Error, VerifyOtpRequest>, 'mutationFn'>
449
+ ): UseMutationResult<VerifyOtpResponse, Error, VerifyOtpRequest> {
450
+ const queryClient = useQueryClient();
451
+
452
+ return useMutation({
453
+ mutationFn: async (data: VerifyOtpRequest): Promise<VerifyOtpResponse> => {
454
+ const client = getApiClient();
455
+ const response = await client.post<VerifyOtpResponse>('/auth/phone/verify-otp', data);
456
+ return response.data;
457
+ },
458
+ onSuccess: (data, variables, context) => {
459
+ // Invalidate current user query to trigger refetch with new token
460
+ queryClient.invalidateQueries({ queryKey: userKeys.me() });
461
+ },
462
+ ...options,
463
+ });
464
+ }
@@ -3,10 +3,10 @@ import { getApiClient } from '../client';
3
3
  import { eventChatKeys } from '../queries/event-chat';
4
4
  import type {
5
5
  ChatMemberResponse,
6
- MessageResponse,
6
+ ChatMessageResponse,
7
7
  EventChatResponse,
8
- SendMessageRequest,
9
- UpdateMessageRequest,
8
+ SendChatMessageRequest,
9
+ UpdateChatMessageRequest,
10
10
  UpdateChatSettingsRequest,
11
11
  UpdateMemberSettingsRequest,
12
12
  } from '../types';
@@ -123,9 +123,9 @@ export function useSendEventChatMessage(eventId: string) {
123
123
  const queryClient = useQueryClient();
124
124
 
125
125
  return useMutation({
126
- mutationFn: async (data: SendMessageRequest) => {
126
+ mutationFn: async (data: SendChatMessageRequest) => {
127
127
  const client = getApiClient();
128
- const response = await client.post<MessageResponse>(
128
+ const response = await client.post<ChatMessageResponse>(
129
129
  `/events/${eventId}/chat/messages`,
130
130
  data
131
131
  );
@@ -145,9 +145,9 @@ export function useUpdateEventChatMessage(eventId: string) {
145
145
  const queryClient = useQueryClient();
146
146
 
147
147
  return useMutation({
148
- mutationFn: async ({ messageId, content }: { messageId: string } & UpdateMessageRequest) => {
148
+ mutationFn: async ({ messageId, content }: { messageId: string } & UpdateChatMessageRequest) => {
149
149
  const client = getApiClient();
150
- const response = await client.put<MessageResponse>(
150
+ const response = await client.put<ChatMessageResponse>(
151
151
  `/events/${eventId}/chat/messages/${messageId}`,
152
152
  { content }
153
153
  );
@@ -6,33 +6,13 @@ import {
6
6
  } from '@tanstack/react-query';
7
7
  import { getApiClient } from '../client';
8
8
  import { matchingKeys, MatchResponse, BuddyResponse } from '../queries/matching';
9
-
10
- // ============================================================================
11
- // REQUEST TYPES
12
- // ============================================================================
13
-
14
- export interface CreateMatchRequest {
15
- matchedUserId: string;
16
- message?: string;
17
- }
18
-
19
- export interface UpdateMatchRequest {
20
- status: 'ACCEPTED' | 'DECLINED' | 'BLOCKED';
21
- }
22
-
23
- export interface CreateBuddyRequest {
24
- buddyId: string;
25
- message?: string;
26
- }
27
-
28
- export interface UpdateBuddyRequest {
29
- status: 'ACCEPTED' | 'DECLINED' | 'ENDED';
30
- }
31
-
32
- export interface LogBuddyActivityRequest {
33
- type: 'CHECK_IN' | 'MESSAGE' | 'SUPPORT';
34
- note?: string;
35
- }
9
+ import type {
10
+ CreateMatchRequest,
11
+ UpdateMatchRequest,
12
+ CreateBuddyRequest,
13
+ UpdateBuddyRequest,
14
+ LogBuddyActivityRequest,
15
+ } from '../types';
36
16
 
37
17
  // ============================================================================
38
18
  // MUTATION HOOKS
@@ -118,9 +98,9 @@ export function useAcceptMatch(
118
98
  return {
119
99
  ...updateMatch,
120
100
  mutate: (matchId: string, mutateOptions?: any) =>
121
- updateMatch.mutate({ matchId, data: { status: 'ACCEPTED' } }, mutateOptions),
101
+ updateMatch.mutate({ matchId, data: { action: 'ACCEPT' } }, mutateOptions),
122
102
  mutateAsync: (matchId: string) =>
123
- updateMatch.mutateAsync({ matchId, data: { status: 'ACCEPTED' } }),
103
+ updateMatch.mutateAsync({ matchId, data: { action: 'ACCEPT' } }),
124
104
  } as UseMutationResult<MatchResponse, Error, string>;
125
105
  }
126
106
 
@@ -141,9 +121,9 @@ export function useDeclineMatch(
141
121
  return {
142
122
  ...updateMatch,
143
123
  mutate: (matchId: string, mutateOptions?: any) =>
144
- updateMatch.mutate({ matchId, data: { status: 'DECLINED' } }, mutateOptions),
124
+ updateMatch.mutate({ matchId, data: { action: 'DECLINE' } }, mutateOptions),
145
125
  mutateAsync: (matchId: string) =>
146
- updateMatch.mutateAsync({ matchId, data: { status: 'DECLINED' } }),
126
+ updateMatch.mutateAsync({ matchId, data: { action: 'DECLINE' } }),
147
127
  } as UseMutationResult<MatchResponse, Error, string>;
148
128
  }
149
129
 
@@ -214,7 +194,7 @@ export function useRequestBuddy(
214
194
  * @example
215
195
  * ```tsx
216
196
  * const updateBuddy = useUpdateBuddy();
217
- * await updateBuddy.mutateAsync({ buddyId: 'buddy-123', data: { status: 'ACCEPTED' } });
197
+ * await updateBuddy.mutateAsync({ buddyId: 'buddy-123', data: { action: 'ACCEPT' } });
218
198
  * ```
219
199
  */
220
200
  export function useUpdateBuddy(
@@ -6,11 +6,11 @@ import {
6
6
  } from '@tanstack/react-query';
7
7
  import { getApiClient } from '../client';
8
8
  import type {
9
- DailyCheckInResponse,
9
+ CheckInResponse,
10
10
  CreateCheckInRequest,
11
11
  UpdateCheckInRequest,
12
12
  MoodLogResponse,
13
- CreateMoodLogRequest,
13
+ LogMoodRequest,
14
14
  WinResponse,
15
15
  CreateWinRequest,
16
16
  HabitResponse,
@@ -44,14 +44,14 @@ import { supportKeys } from '../queries/support';
44
44
  */
45
45
  export function useCreateCheckIn(
46
46
  options?: Omit<
47
- UseMutationOptions<DailyCheckInResponse, Error, CreateCheckInRequest>,
47
+ UseMutationOptions<CheckInResponse, Error, CreateCheckInRequest>,
48
48
  'mutationFn'
49
49
  >
50
- ): UseMutationResult<DailyCheckInResponse, Error, CreateCheckInRequest> {
50
+ ): UseMutationResult<CheckInResponse, Error, CreateCheckInRequest> {
51
51
  const queryClient = useQueryClient();
52
52
 
53
53
  return useMutation({
54
- mutationFn: async (data: CreateCheckInRequest): Promise<DailyCheckInResponse> => {
54
+ mutationFn: async (data: CreateCheckInRequest): Promise<CheckInResponse> => {
55
55
  const client = getApiClient();
56
56
  const response = await client.post('/api/v1/support/check-ins', data);
57
57
  return response.data;
@@ -88,14 +88,14 @@ export function useCreateCheckIn(
88
88
  */
89
89
  export function useUpdateCheckIn(
90
90
  options?: Omit<
91
- UseMutationOptions<DailyCheckInResponse, Error, { id: string; data: UpdateCheckInRequest }>,
91
+ UseMutationOptions<CheckInResponse, Error, { id: string; data: UpdateCheckInRequest }>,
92
92
  'mutationFn'
93
93
  >
94
- ): UseMutationResult<DailyCheckInResponse, Error, { id: string; data: UpdateCheckInRequest }> {
94
+ ): UseMutationResult<CheckInResponse, Error, { id: string; data: UpdateCheckInRequest }> {
95
95
  const queryClient = useQueryClient();
96
96
 
97
97
  return useMutation({
98
- mutationFn: async ({ id, data }: { id: string; data: UpdateCheckInRequest }): Promise<DailyCheckInResponse> => {
98
+ mutationFn: async ({ id, data }: { id: string; data: UpdateCheckInRequest }): Promise<CheckInResponse> => {
99
99
  const client = getApiClient();
100
100
  const response = await client.put(`/api/v1/support/check-ins/${id}`, data);
101
101
  return response.data;
@@ -128,14 +128,14 @@ export function useUpdateCheckIn(
128
128
  */
129
129
  export function useCreateMoodLog(
130
130
  options?: Omit<
131
- UseMutationOptions<MoodLogResponse, Error, CreateMoodLogRequest>,
131
+ UseMutationOptions<MoodLogResponse, Error, LogMoodRequest>,
132
132
  'mutationFn'
133
133
  >
134
- ): UseMutationResult<MoodLogResponse, Error, CreateMoodLogRequest> {
134
+ ): UseMutationResult<MoodLogResponse, Error, LogMoodRequest> {
135
135
  const queryClient = useQueryClient();
136
136
 
137
137
  return useMutation({
138
- mutationFn: async (data: CreateMoodLogRequest): Promise<MoodLogResponse> => {
138
+ mutationFn: async (data: LogMoodRequest): Promise<MoodLogResponse> => {
139
139
  const client = getApiClient();
140
140
  const response = await client.post('/api/v1/support/mood', data);
141
141
  return response.data;
@@ -1,6 +1,6 @@
1
1
  import { useQuery, UseQueryOptions } from '@tanstack/react-query';
2
2
  import { getApiClient } from '../client';
3
- import type { AmbassadorResponse, AmbassadorLeaderboardResponse, AmbassadorStatus } from '../types';
3
+ import type { AmbassadorResponse } from '../types';
4
4
 
5
5
  // ============================================================================
6
6
  // QUERY KEYS
@@ -21,6 +21,9 @@ export const ambassadorKeys = {
21
21
  // TYPES
22
22
  // ============================================================================
23
23
 
24
+ // Extract status type from AmbassadorResponse
25
+ type AmbassadorStatus = AmbassadorResponse['status'];
26
+
24
27
  export interface AmbassadorListFilters {
25
28
  page?: number;
26
29
  limit?: number;
@@ -108,11 +111,11 @@ export function useMyAmbassador(
108
111
  */
109
112
  export function useAmbassadorLeaderboard(
110
113
  filters?: LeaderboardFilters,
111
- options?: Omit<UseQueryOptions<AmbassadorLeaderboardResponse[]>, 'queryKey' | 'queryFn'>
114
+ options?: Omit<UseQueryOptions<AmbassadorResponse[]>, 'queryKey' | 'queryFn'>
112
115
  ) {
113
116
  return useQuery({
114
117
  queryKey: ambassadorKeys.leaderboard(filters),
115
- queryFn: async (): Promise<AmbassadorLeaderboardResponse[]> => {
118
+ queryFn: async (): Promise<AmbassadorResponse[]> => {
116
119
  const client = getApiClient();
117
120
  const response = await client.get('/api/v1/ambassadors/leaderboard', { params: filters });
118
121
  return response.data;
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Cities Query Hooks
3
+ *
4
+ * TanStack Query hooks for fetching city data.
5
+ * Cities are used for location-based features and user onboarding.
6
+ *
7
+ * @module api/queries/cities
8
+ */
9
+
10
+ import { useQuery, UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
11
+ import { getApiClient } from '../client';
12
+
13
+ // ============================================================================
14
+ // QUERY KEY FACTORY
15
+ // ============================================================================
16
+
17
+ export const cityKeys = {
18
+ all: ['cities'] as const,
19
+ lists: () => [...cityKeys.all, 'list'] as const,
20
+ list: (filters?: CityFilters) => [...cityKeys.lists(), filters] as const,
21
+ details: () => [...cityKeys.all, 'detail'] as const,
22
+ detail: (id: string) => [...cityKeys.details(), id] as const,
23
+ featured: () => [...cityKeys.all, 'featured'] as const,
24
+ };
25
+
26
+ // ============================================================================
27
+ // TYPES
28
+ // ============================================================================
29
+
30
+ export interface CityResponse {
31
+ id: string;
32
+ name: string;
33
+ country: string;
34
+ countryCode: string | null;
35
+ timezone: string | null;
36
+ latitude: number | null;
37
+ longitude: number | null;
38
+ memberCount: number;
39
+ isFeatured: boolean;
40
+ region: string | null;
41
+ }
42
+
43
+ export interface CityFilters {
44
+ search?: string;
45
+ countryCode?: string;
46
+ isFeatured?: boolean;
47
+ }
48
+
49
+ // ============================================================================
50
+ // QUERY HOOKS
51
+ // ============================================================================
52
+
53
+ /**
54
+ * Get list of cities with optional filters
55
+ *
56
+ * @description
57
+ * Fetches available cities for location selection.
58
+ * Can be filtered by name search, country code, or featured status.
59
+ * Results are ordered by featured status, member count, and name.
60
+ *
61
+ * @endpoint GET /api/v1/cities
62
+ *
63
+ * @example
64
+ * ```tsx
65
+ * import { useCities } from '@growsober/sdk';
66
+ *
67
+ * function CityPicker() {
68
+ * const { data: cities, isLoading } = useCities();
69
+ *
70
+ * if (isLoading) return <Spinner />;
71
+ *
72
+ * return (
73
+ * <select>
74
+ * {cities?.map(city => (
75
+ * <option key={city.id} value={city.id}>
76
+ * {city.name}, {city.country}
77
+ * </option>
78
+ * ))}
79
+ * </select>
80
+ * );
81
+ * }
82
+ * ```
83
+ *
84
+ * @example
85
+ * With search filter:
86
+ * ```tsx
87
+ * const { data: cities } = useCities({ search: 'London' });
88
+ * ```
89
+ *
90
+ * @example
91
+ * Filter by country:
92
+ * ```tsx
93
+ * const { data: ukCities } = useCities({ countryCode: 'GB' });
94
+ * ```
95
+ *
96
+ * @param filters - Optional filters for city search
97
+ * @param options - TanStack Query options
98
+ * @returns TanStack Query result with array of cities
99
+ */
100
+ interface CitiesApiResponse {
101
+ data: CityResponse[];
102
+ meta: {
103
+ timestamp: string;
104
+ };
105
+ }
106
+
107
+ export function useCities(
108
+ filters?: CityFilters,
109
+ options?: Omit<UseQueryOptions<CityResponse[]>, 'queryKey' | 'queryFn'>
110
+ ): UseQueryResult<CityResponse[]> {
111
+ return useQuery({
112
+ queryKey: cityKeys.list(filters),
113
+ queryFn: async (): Promise<CityResponse[]> => {
114
+ const client = getApiClient();
115
+ const params = new URLSearchParams();
116
+
117
+ if (filters?.search) params.append('search', filters.search);
118
+ if (filters?.countryCode) params.append('countryCode', filters.countryCode);
119
+ if (filters?.isFeatured !== undefined) params.append('isFeatured', String(filters.isFeatured));
120
+
121
+ const queryString = params.toString();
122
+ const url = `/api/v1/cities${queryString ? `?${queryString}` : ''}`;
123
+
124
+ const response = await client.get<CitiesApiResponse>(url);
125
+ // Handle both wrapped and unwrapped response formats
126
+ return Array.isArray(response.data) ? response.data : response.data.data;
127
+ },
128
+ staleTime: 5 * 60 * 1000, // Cities don't change often, cache for 5 minutes
129
+ ...options,
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Get featured cities
135
+ *
136
+ * @description
137
+ * Fetches only cities marked as featured.
138
+ * Useful for showing popular/recommended cities.
139
+ *
140
+ * @example
141
+ * ```tsx
142
+ * const { data: featuredCities } = useFeaturedCities();
143
+ * ```
144
+ *
145
+ * @param options - TanStack Query options
146
+ * @returns TanStack Query result with array of featured cities
147
+ */
148
+ export function useFeaturedCities(
149
+ options?: Omit<UseQueryOptions<CityResponse[]>, 'queryKey' | 'queryFn'>
150
+ ): UseQueryResult<CityResponse[]> {
151
+ return useCities({ isFeatured: true }, options);
152
+ }
153
+
154
+ /**
155
+ * Get a single city by ID
156
+ *
157
+ * @description
158
+ * Fetches details for a specific city.
159
+ *
160
+ * @endpoint GET /api/v1/cities/{id}
161
+ *
162
+ * @example
163
+ * ```tsx
164
+ * const { data: city } = useCity('city-uuid');
165
+ * ```
166
+ *
167
+ * @param id - City ID
168
+ * @param options - TanStack Query options
169
+ * @returns TanStack Query result with city data
170
+ */
171
+ interface CityApiResponse {
172
+ data: CityResponse;
173
+ meta: {
174
+ timestamp: string;
175
+ };
176
+ }
177
+
178
+ export function useCity(
179
+ id: string,
180
+ options?: Omit<UseQueryOptions<CityResponse>, 'queryKey' | 'queryFn'>
181
+ ): UseQueryResult<CityResponse> {
182
+ return useQuery({
183
+ queryKey: cityKeys.detail(id),
184
+ queryFn: async (): Promise<CityResponse> => {
185
+ const client = getApiClient();
186
+ const response = await client.get<CityApiResponse>(`/api/v1/cities/${id}`);
187
+ // Handle both wrapped and unwrapped response formats
188
+ return (response.data as any).data || response.data;
189
+ },
190
+ enabled: !!id,
191
+ staleTime: 5 * 60 * 1000,
192
+ ...options,
193
+ });
194
+ }
@@ -1,6 +1,6 @@
1
1
  import { useQuery, UseQueryOptions } from '@tanstack/react-query';
2
2
  import { getApiClient } from '../client';
3
- import type { HubResponse, HubMemberResponse } from '../types';
3
+ import type { HubResponse, ChatMemberResponse } from '../types';
4
4
 
5
5
  // ============================================================================
6
6
  // QUERY KEYS
@@ -49,10 +49,10 @@ export interface PaginatedHubsResponse {
49
49
  };
50
50
  }
51
51
 
52
- // HubMemberResponse is imported from '../types'
52
+ // ChatMemberResponse is imported from '../types'
53
53
 
54
54
  export interface PaginatedMembersResponse {
55
- data: HubMemberResponse[];
55
+ data: ChatMemberResponse[];
56
56
  meta: {
57
57
  total: number;
58
58
  page: number;
@@ -8,6 +8,7 @@ export * from './admin';
8
8
  export * from './auth';
9
9
  export * from './bookings';
10
10
  export * from './businesses';
11
+ export * from './cities';
11
12
  export * from './events';
12
13
  export * from './hubs';
13
14
  export * from './library';
@@ -11,10 +11,10 @@
11
11
  import { useQuery, UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
12
12
  import { getApiClient } from '../client';
13
13
  import type {
14
- MapUserResponse,
14
+ MapMemberResponse,
15
15
  MapEventResponse,
16
16
  MapHubResponse,
17
- MapBusinessResponse,
17
+ BusinessResponse,
18
18
  } from '../types';
19
19
 
20
20
  // ============================================================================
@@ -115,13 +115,13 @@ export interface MapBusinessesParams {
115
115
  */
116
116
  export function useMapMembers(
117
117
  params: MapMembersParams,
118
- options?: Omit<UseQueryOptions<MapUserResponse[]>, 'queryKey' | 'queryFn'>
119
- ): UseQueryResult<MapUserResponse[]> {
118
+ options?: Omit<UseQueryOptions<MapMemberResponse[]>, 'queryKey' | 'queryFn'>
119
+ ): UseQueryResult<MapMemberResponse[]> {
120
120
  return useQuery({
121
121
  queryKey: mapKeys.members(params),
122
- queryFn: async (): Promise<MapUserResponse[]> => {
122
+ queryFn: async (): Promise<MapMemberResponse[]> => {
123
123
  const client = getApiClient();
124
- const response = await client.get<MapUserResponse[]>('/api/v1/map/members', {
124
+ const response = await client.get<MapMemberResponse[]>('/api/v1/map/members', {
125
125
  params: {
126
126
  lat: params.lat,
127
127
  lng: params.lng,
@@ -308,13 +308,13 @@ export function useMapHubs(
308
308
  */
309
309
  export function useMapBusinesses(
310
310
  params: MapBusinessesParams,
311
- options?: Omit<UseQueryOptions<MapBusinessResponse[]>, 'queryKey' | 'queryFn'>
312
- ): UseQueryResult<MapBusinessResponse[]> {
311
+ options?: Omit<UseQueryOptions<BusinessResponse[]>, 'queryKey' | 'queryFn'>
312
+ ): UseQueryResult<BusinessResponse[]> {
313
313
  return useQuery({
314
314
  queryKey: mapKeys.businesses(params),
315
- queryFn: async (): Promise<MapBusinessResponse[]> => {
315
+ queryFn: async (): Promise<BusinessResponse[]> => {
316
316
  const client = getApiClient();
317
- const response = await client.get<MapBusinessResponse[]>('/api/v1/map/businesses', {
317
+ const response = await client.get<BusinessResponse[]>('/api/v1/map/businesses', {
318
318
  params: {
319
319
  lat: params.lat,
320
320
  lng: params.lng,