@spotsdev/sdk 1.0.0

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 (79) hide show
  1. package/dist/api/client.d.ts +12 -0
  2. package/dist/api/client.js +68 -0
  3. package/dist/api/mutations/clubs.d.ts +47 -0
  4. package/dist/api/mutations/clubs.js +95 -0
  5. package/dist/api/mutations/conversations.d.ts +45 -0
  6. package/dist/api/mutations/conversations.js +110 -0
  7. package/dist/api/mutations/index.d.ts +13 -0
  8. package/dist/api/mutations/index.js +38 -0
  9. package/dist/api/mutations/notifications.d.ts +38 -0
  10. package/dist/api/mutations/notifications.js +64 -0
  11. package/dist/api/mutations/orders.d.ts +73 -0
  12. package/dist/api/mutations/orders.js +116 -0
  13. package/dist/api/mutations/posts.d.ts +123 -0
  14. package/dist/api/mutations/posts.js +229 -0
  15. package/dist/api/mutations/products.d.ts +81 -0
  16. package/dist/api/mutations/products.js +102 -0
  17. package/dist/api/mutations/spots.d.ts +59 -0
  18. package/dist/api/mutations/spots.js +129 -0
  19. package/dist/api/mutations/users.d.ts +71 -0
  20. package/dist/api/mutations/users.js +173 -0
  21. package/dist/api/queries/auth.d.ts +37 -0
  22. package/dist/api/queries/auth.js +61 -0
  23. package/dist/api/queries/clubs.d.ts +52 -0
  24. package/dist/api/queries/clubs.js +116 -0
  25. package/dist/api/queries/conversations.d.ts +52 -0
  26. package/dist/api/queries/conversations.js +83 -0
  27. package/dist/api/queries/index.d.ts +26 -0
  28. package/dist/api/queries/index.js +65 -0
  29. package/dist/api/queries/misc.d.ts +54 -0
  30. package/dist/api/queries/misc.js +129 -0
  31. package/dist/api/queries/notifications.d.ts +34 -0
  32. package/dist/api/queries/notifications.js +62 -0
  33. package/dist/api/queries/orders.d.ts +45 -0
  34. package/dist/api/queries/orders.js +93 -0
  35. package/dist/api/queries/posts.d.ts +55 -0
  36. package/dist/api/queries/posts.js +130 -0
  37. package/dist/api/queries/products.d.ts +52 -0
  38. package/dist/api/queries/products.js +89 -0
  39. package/dist/api/queries/spots.d.ts +78 -0
  40. package/dist/api/queries/spots.js +168 -0
  41. package/dist/api/queries/templates.d.ts +42 -0
  42. package/dist/api/queries/templates.js +86 -0
  43. package/dist/api/queries/users.d.ts +90 -0
  44. package/dist/api/queries/users.js +187 -0
  45. package/dist/api/services/index.d.ts +2 -0
  46. package/dist/api/services/index.js +8 -0
  47. package/dist/api/services/marketplace.d.ts +129 -0
  48. package/dist/api/services/marketplace.js +168 -0
  49. package/dist/api/types.d.ts +54 -0
  50. package/dist/api/types.js +34 -0
  51. package/dist/index.d.ts +38 -0
  52. package/dist/index.js +73 -0
  53. package/package.json +57 -0
  54. package/src/api/client.ts +78 -0
  55. package/src/api/mutations/clubs.ts +107 -0
  56. package/src/api/mutations/conversations.ts +124 -0
  57. package/src/api/mutations/index.ts +29 -0
  58. package/src/api/mutations/notifications.ts +70 -0
  59. package/src/api/mutations/orders.ts +174 -0
  60. package/src/api/mutations/posts.ts +278 -0
  61. package/src/api/mutations/products.ts +160 -0
  62. package/src/api/mutations/spots.ts +146 -0
  63. package/src/api/mutations/users.ts +197 -0
  64. package/src/api/queries/auth.ts +67 -0
  65. package/src/api/queries/clubs.ts +135 -0
  66. package/src/api/queries/conversations.ts +94 -0
  67. package/src/api/queries/index.ts +48 -0
  68. package/src/api/queries/misc.ts +140 -0
  69. package/src/api/queries/notifications.ts +66 -0
  70. package/src/api/queries/orders.ts +119 -0
  71. package/src/api/queries/posts.ts +142 -0
  72. package/src/api/queries/products.ts +123 -0
  73. package/src/api/queries/spots.ts +201 -0
  74. package/src/api/queries/templates.ts +95 -0
  75. package/src/api/queries/users.ts +206 -0
  76. package/src/api/services/index.ts +6 -0
  77. package/src/api/services/marketplace.ts +265 -0
  78. package/src/api/types.ts +144 -0
  79. package/src/index.ts +63 -0
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Misc Query Hooks
3
+ *
4
+ * TanStack Query hooks for cities, vibes, and other reference data.
5
+ * All types come from @prisma/client (via ../types re-export).
6
+ *
7
+ * API Response Patterns:
8
+ * - Reference data (vibes, templates, interests): { success, data: T[], timestamp }
9
+ * - Large datasets (cities, spots, posts): { success, data: { data: T[], meta }, timestamp }
10
+ */
11
+
12
+ import { useQuery, UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
13
+ import { getApiClient } from '../client';
14
+ import type { City, Vibe, LifeSituation, Interest, Intention, ApiResponse, PaginatedResponse } from '../types';
15
+
16
+ // ============================================================================
17
+ // QUERY KEYS
18
+ // ============================================================================
19
+
20
+ export const miscKeys = {
21
+ cities: () => ['cities'] as const,
22
+ vibes: () => ['vibes'] as const,
23
+ lifeSituations: () => ['life-situations'] as const,
24
+ interests: () => ['interests'] as const,
25
+ intentions: () => ['intentions'] as const,
26
+ };
27
+
28
+ // ============================================================================
29
+ // QUERY HOOKS
30
+ // ============================================================================
31
+
32
+ /**
33
+ * Get all cities (paginated endpoint)
34
+ *
35
+ * @endpoint GET /api/v1/cities
36
+ * @returns PaginatedResponse with cities array
37
+ */
38
+ export function useCities(
39
+ options?: Omit<UseQueryOptions<City[]>, 'queryKey' | 'queryFn'>
40
+ ): UseQueryResult<City[]> {
41
+ return useQuery({
42
+ queryKey: miscKeys.cities(),
43
+ queryFn: async (): Promise<City[]> => {
44
+ const client = getApiClient();
45
+ const response = await client.get<ApiResponse<PaginatedResponse<City>>>('/api/v1/cities');
46
+ // Cities endpoint returns paginated format: { data: { data: [], meta: {} } }
47
+ return response.data.data.data;
48
+ },
49
+ staleTime: 1000 * 60 * 60,
50
+ ...options,
51
+ });
52
+ }
53
+
54
+ /**
55
+ * Get all vibes (flat array - reference data)
56
+ *
57
+ * @endpoint GET /api/v1/vibes
58
+ * @returns Flat array of vibes (not paginated)
59
+ */
60
+ export function useVibes(
61
+ options?: Omit<UseQueryOptions<Vibe[]>, 'queryKey' | 'queryFn'>
62
+ ): UseQueryResult<Vibe[]> {
63
+ return useQuery({
64
+ queryKey: miscKeys.vibes(),
65
+ queryFn: async (): Promise<Vibe[]> => {
66
+ const client = getApiClient();
67
+ const response = await client.get<ApiResponse<Vibe[]>>('/api/v1/vibes');
68
+ // Vibes endpoint returns flat array: { data: [] }
69
+ return response.data.data;
70
+ },
71
+ staleTime: 1000 * 60 * 60,
72
+ ...options,
73
+ });
74
+ }
75
+
76
+ /**
77
+ * Get all life situations (flat array - reference data for onboarding)
78
+ *
79
+ * @endpoint GET /api/v1/life-situations
80
+ * @returns Flat array of life situations (not paginated)
81
+ */
82
+ export function useLifeSituations(
83
+ options?: Omit<UseQueryOptions<LifeSituation[]>, 'queryKey' | 'queryFn'>
84
+ ): UseQueryResult<LifeSituation[]> {
85
+ return useQuery({
86
+ queryKey: miscKeys.lifeSituations(),
87
+ queryFn: async (): Promise<LifeSituation[]> => {
88
+ const client = getApiClient();
89
+ const response = await client.get<ApiResponse<LifeSituation[]>>('/api/v1/life-situations');
90
+ // Life situations endpoint returns flat array: { data: [] }
91
+ return response.data.data;
92
+ },
93
+ staleTime: 1000 * 60 * 60,
94
+ ...options,
95
+ });
96
+ }
97
+
98
+ /**
99
+ * Get all interests (flat array - reference data)
100
+ *
101
+ * @endpoint GET /api/v1/interests
102
+ * @returns Flat array of interests (not paginated)
103
+ */
104
+ export function useInterests(
105
+ options?: Omit<UseQueryOptions<Interest[]>, 'queryKey' | 'queryFn'>
106
+ ): UseQueryResult<Interest[]> {
107
+ return useQuery({
108
+ queryKey: miscKeys.interests(),
109
+ queryFn: async (): Promise<Interest[]> => {
110
+ const client = getApiClient();
111
+ const response = await client.get<ApiResponse<Interest[]>>('/api/v1/interests');
112
+ // Interests endpoint returns flat array: { data: [] }
113
+ return response.data.data;
114
+ },
115
+ staleTime: 1000 * 60 * 60,
116
+ ...options,
117
+ });
118
+ }
119
+
120
+ /**
121
+ * Get all intentions (flat array - reference data)
122
+ *
123
+ * @endpoint GET /api/v1/intentions
124
+ * @returns Flat array of intentions (not paginated)
125
+ */
126
+ export function useIntentions(
127
+ options?: Omit<UseQueryOptions<Intention[]>, 'queryKey' | 'queryFn'>
128
+ ): UseQueryResult<Intention[]> {
129
+ return useQuery({
130
+ queryKey: miscKeys.intentions(),
131
+ queryFn: async (): Promise<Intention[]> => {
132
+ const client = getApiClient();
133
+ const response = await client.get<ApiResponse<Intention[]>>('/api/v1/intentions');
134
+ // Intentions endpoint returns flat array: { data: [] }
135
+ return response.data.data;
136
+ },
137
+ staleTime: 1000 * 60 * 60,
138
+ ...options,
139
+ });
140
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Notifications Query Hooks
3
+ *
4
+ * TanStack Query hooks for notification operations.
5
+ */
6
+
7
+ import { useQuery, UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
8
+ import { getApiClient } from '../client';
9
+ import type { Notification, ApiResponse } from '../types';
10
+
11
+ // ============================================================================
12
+ // QUERY KEYS
13
+ // ============================================================================
14
+
15
+ export const notificationKeys = {
16
+ all: ['notifications'] as const,
17
+ lists: () => [...notificationKeys.all, 'list'] as const,
18
+ list: (params?: { limit?: number; unreadOnly?: boolean }) => [...notificationKeys.lists(), params] as const,
19
+ unreadCount: () => [...notificationKeys.all, 'unreadCount'] as const,
20
+ };
21
+
22
+ // ============================================================================
23
+ // QUERY HOOKS
24
+ // ============================================================================
25
+
26
+ /**
27
+ * Get notifications for current user
28
+ *
29
+ * @endpoint GET /api/v1/notifications
30
+ */
31
+ export function useNotifications(
32
+ params?: { limit?: number; unreadOnly?: boolean },
33
+ options?: Omit<UseQueryOptions<Notification[]>, 'queryKey' | 'queryFn'>
34
+ ): UseQueryResult<Notification[]> {
35
+ return useQuery({
36
+ queryKey: notificationKeys.list(params),
37
+ queryFn: async (): Promise<Notification[]> => {
38
+ const client = getApiClient();
39
+ const queryParams = new URLSearchParams();
40
+ if (params?.limit) queryParams.set('limit', String(params.limit));
41
+ if (params?.unreadOnly) queryParams.set('unreadOnly', String(params.unreadOnly));
42
+ const response = await client.get<ApiResponse<Notification[]>>(`/api/v1/notifications?${queryParams}`);
43
+ return response.data.data;
44
+ },
45
+ ...options,
46
+ });
47
+ }
48
+
49
+ /**
50
+ * Get unread notification count
51
+ *
52
+ * @endpoint GET /api/v1/notifications/unread-count
53
+ */
54
+ export function useUnreadNotificationCount(
55
+ options?: Omit<UseQueryOptions<number>, 'queryKey' | 'queryFn'>
56
+ ): UseQueryResult<number> {
57
+ return useQuery({
58
+ queryKey: notificationKeys.unreadCount(),
59
+ queryFn: async (): Promise<number> => {
60
+ const client = getApiClient();
61
+ const response = await client.get<ApiResponse<{ count: number }>>('/api/v1/notifications/unread-count');
62
+ return response.data.data.count;
63
+ },
64
+ ...options,
65
+ });
66
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Orders Query Hooks
3
+ *
4
+ * TanStack Query hooks for order-related operations.
5
+ */
6
+
7
+ import { useQuery, UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
8
+ import { getApiClient } from '../client';
9
+ import type { Order, OrderItem, Product, Spot, OrderStatus, ApiResponse, PaginatedResponse } from '../types';
10
+
11
+ // ============================================================================
12
+ // QUERY KEYS
13
+ // ============================================================================
14
+
15
+ export const orderKeys = {
16
+ all: ['orders'] as const,
17
+ lists: () => [...orderKeys.all, 'list'] as const,
18
+ list: (filters?: Record<string, unknown>) => [...orderKeys.lists(), filters] as const,
19
+ details: () => [...orderKeys.all, 'detail'] as const,
20
+ detail: (id: string) => [...orderKeys.details(), id] as const,
21
+ myOrders: () => [...orderKeys.all, 'my'] as const,
22
+ spotOrders: (spotId: string) => [...orderKeys.all, 'spot', spotId] as const,
23
+ };
24
+
25
+ // ============================================================================
26
+ // TYPES
27
+ // ============================================================================
28
+
29
+ export interface OrderWithDetails extends Order {
30
+ items: (OrderItem & {
31
+ product: Pick<Product, 'id' | 'name' | 'slug' | 'type' | 'imageUrl'>;
32
+ })[];
33
+ spot: Pick<Spot, 'id' | 'name' | 'slug'>;
34
+ }
35
+
36
+ export interface OrderFilters {
37
+ status?: OrderStatus;
38
+ limit?: number;
39
+ page?: number;
40
+ }
41
+
42
+ // ============================================================================
43
+ // QUERY HOOKS
44
+ // ============================================================================
45
+
46
+ /**
47
+ * Get current user's orders (purchases)
48
+ *
49
+ * @endpoint GET /api/v1/users/me/orders
50
+ */
51
+ export function useMyOrders(
52
+ params?: OrderFilters,
53
+ options?: Omit<UseQueryOptions<PaginatedResponse<OrderWithDetails>>, 'queryKey' | 'queryFn'>
54
+ ): UseQueryResult<PaginatedResponse<OrderWithDetails>> {
55
+ return useQuery({
56
+ queryKey: orderKeys.list({ ...params, my: true }),
57
+ queryFn: async (): Promise<PaginatedResponse<OrderWithDetails>> => {
58
+ const client = getApiClient();
59
+ const queryParams = new URLSearchParams();
60
+ if (params?.status) queryParams.set('status', params.status);
61
+ if (params?.limit) queryParams.set('limit', String(params.limit));
62
+ if (params?.page) queryParams.set('page', String(params.page));
63
+ const response = await client.get<ApiResponse<PaginatedResponse<OrderWithDetails>>>(
64
+ `/api/v1/users/me/orders?${queryParams}`
65
+ );
66
+ return response.data.data;
67
+ },
68
+ ...options,
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Get a single order by ID
74
+ *
75
+ * @endpoint GET /api/v1/orders/{orderId}
76
+ */
77
+ export function useOrder(
78
+ orderId: string,
79
+ options?: Omit<UseQueryOptions<OrderWithDetails>, 'queryKey' | 'queryFn'>
80
+ ): UseQueryResult<OrderWithDetails> {
81
+ return useQuery({
82
+ queryKey: orderKeys.detail(orderId),
83
+ queryFn: async (): Promise<OrderWithDetails> => {
84
+ const client = getApiClient();
85
+ const response = await client.get<ApiResponse<OrderWithDetails>>(`/api/v1/orders/${orderId}`);
86
+ return response.data.data;
87
+ },
88
+ enabled: !!orderId,
89
+ ...options,
90
+ });
91
+ }
92
+
93
+ /**
94
+ * Get orders for a spot (seller view)
95
+ *
96
+ * @endpoint GET /api/v1/seller/spots/{spotId}/orders
97
+ */
98
+ export function useSpotOrders(
99
+ spotId: string,
100
+ params?: OrderFilters,
101
+ options?: Omit<UseQueryOptions<PaginatedResponse<OrderWithDetails>>, 'queryKey' | 'queryFn'>
102
+ ): UseQueryResult<PaginatedResponse<OrderWithDetails>> {
103
+ return useQuery({
104
+ queryKey: orderKeys.spotOrders(spotId),
105
+ queryFn: async (): Promise<PaginatedResponse<OrderWithDetails>> => {
106
+ const client = getApiClient();
107
+ const queryParams = new URLSearchParams();
108
+ if (params?.status) queryParams.set('status', params.status);
109
+ if (params?.limit) queryParams.set('limit', String(params.limit));
110
+ if (params?.page) queryParams.set('page', String(params.page));
111
+ const response = await client.get<ApiResponse<PaginatedResponse<OrderWithDetails>>>(
112
+ `/api/v1/seller/spots/${spotId}/orders?${queryParams}`
113
+ );
114
+ return response.data.data;
115
+ },
116
+ enabled: !!spotId,
117
+ ...options,
118
+ });
119
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Posts Query Hooks
3
+ *
4
+ * TanStack Query hooks for post/board operations.
5
+ */
6
+
7
+ import { useQuery, UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
8
+ import { getApiClient } from '../client';
9
+ import type { Post, PostResponse, ApiResponse, PostStatusDto } from '../types';
10
+
11
+ // ============================================================================
12
+ // QUERY KEYS
13
+ // ============================================================================
14
+
15
+ export const postKeys = {
16
+ all: ['posts'] as const,
17
+ lists: () => [...postKeys.all, 'list'] as const,
18
+ list: (filters?: Record<string, unknown>) => [...postKeys.lists(), filters] as const,
19
+ bySpot: (spotId: string, filters?: Record<string, unknown>) => [...postKeys.all, 'spot', spotId, filters] as const,
20
+ details: () => [...postKeys.all, 'detail'] as const,
21
+ detail: (id: string) => [...postKeys.details(), id] as const,
22
+ responses: (postId: string) => [...postKeys.detail(postId), 'responses'] as const,
23
+ status: (postId: string) => [...postKeys.detail(postId), 'status'] as const,
24
+ };
25
+
26
+ // ============================================================================
27
+ // QUERY HOOKS
28
+ // ============================================================================
29
+
30
+ /**
31
+ * Get posts for a spot
32
+ *
33
+ * @endpoint GET /api/v1/spots/{spotId}/posts
34
+ */
35
+ export function useSpotPosts(
36
+ spotId: string,
37
+ params?: { postType?: string; status?: string; page?: number; limit?: number },
38
+ options?: Omit<UseQueryOptions<Post[]>, 'queryKey' | 'queryFn'>
39
+ ): UseQueryResult<Post[]> {
40
+ return useQuery({
41
+ queryKey: postKeys.bySpot(spotId, params),
42
+ queryFn: async (): Promise<Post[]> => {
43
+ const client = getApiClient();
44
+ const queryParams = new URLSearchParams();
45
+ if (params?.postType) queryParams.set('postType', params.postType);
46
+ if (params?.status) queryParams.set('status', params.status);
47
+ if (params?.page) queryParams.set('page', String(params.page));
48
+ if (params?.limit) queryParams.set('limit', String(params.limit));
49
+ const response = await client.get<ApiResponse<Post[]>>(`/api/v1/spots/${spotId}/posts?${queryParams}`);
50
+ return response.data.data;
51
+ },
52
+ enabled: !!spotId,
53
+ ...options,
54
+ });
55
+ }
56
+
57
+ /**
58
+ * Get a single post by ID
59
+ *
60
+ * @endpoint GET /api/v1/posts/{postId}
61
+ */
62
+ export function usePost(
63
+ postId: string,
64
+ options?: Omit<UseQueryOptions<Post>, 'queryKey' | 'queryFn'>
65
+ ): UseQueryResult<Post> {
66
+ return useQuery({
67
+ queryKey: postKeys.detail(postId),
68
+ queryFn: async (): Promise<Post> => {
69
+ const client = getApiClient();
70
+ const response = await client.get<ApiResponse<Post>>(`/api/v1/posts/${postId}`);
71
+ return response.data.data;
72
+ },
73
+ enabled: !!postId,
74
+ ...options,
75
+ });
76
+ }
77
+
78
+ /**
79
+ * Get responses for a post
80
+ *
81
+ * @endpoint GET /api/v1/posts/{postId}/responses
82
+ */
83
+ export function usePostResponses(
84
+ postId: string,
85
+ options?: Omit<UseQueryOptions<PostResponse[]>, 'queryKey' | 'queryFn'>
86
+ ): UseQueryResult<PostResponse[]> {
87
+ return useQuery({
88
+ queryKey: postKeys.responses(postId),
89
+ queryFn: async (): Promise<PostResponse[]> => {
90
+ const client = getApiClient();
91
+ const response = await client.get<ApiResponse<PostResponse[]>>(`/api/v1/posts/${postId}/responses`);
92
+ return response.data.data;
93
+ },
94
+ enabled: !!postId,
95
+ ...options,
96
+ });
97
+ }
98
+
99
+ /**
100
+ * Get all posts (with filters)
101
+ *
102
+ * @endpoint GET /api/v1/posts
103
+ */
104
+ export function usePosts(
105
+ params?: { postType?: string; limit?: number },
106
+ options?: Omit<UseQueryOptions<Post[]>, 'queryKey' | 'queryFn'>
107
+ ): UseQueryResult<Post[]> {
108
+ return useQuery({
109
+ queryKey: postKeys.list(params),
110
+ queryFn: async (): Promise<Post[]> => {
111
+ const client = getApiClient();
112
+ const queryParams = new URLSearchParams();
113
+ if (params?.postType) queryParams.set('postType', params.postType);
114
+ if (params?.limit) queryParams.set('limit', String(params.limit));
115
+ const response = await client.get<ApiResponse<Post[]>>(`/api/v1/posts?${queryParams}`);
116
+ return response.data.data;
117
+ },
118
+ ...options,
119
+ });
120
+ }
121
+
122
+
123
+ /**
124
+ * Get user's status for a post (read/hidden/pinned)
125
+ *
126
+ * @endpoint GET /api/v1/posts/{postId}/status
127
+ */
128
+ export function usePostStatus(
129
+ postId: string,
130
+ options?: Omit<UseQueryOptions<PostStatusDto>, 'queryKey' | 'queryFn'>
131
+ ): UseQueryResult<PostStatusDto> {
132
+ return useQuery({
133
+ queryKey: postKeys.status(postId),
134
+ queryFn: async (): Promise<PostStatusDto> => {
135
+ const client = getApiClient();
136
+ const response = await client.get<ApiResponse<PostStatusDto>>(`/api/v1/posts/${postId}/status`);
137
+ return response.data.data;
138
+ },
139
+ enabled: !!postId,
140
+ ...options,
141
+ });
142
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Products Query Hooks
3
+ *
4
+ * TanStack Query hooks for product-related operations.
5
+ */
6
+
7
+ import { useQuery, UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
8
+ import { getApiClient } from '../client';
9
+ import type { Product, ApiResponse, PaginatedResponse, ProductType, ProductStatus } from '../types';
10
+
11
+ // ============================================================================
12
+ // QUERY KEYS
13
+ // ============================================================================
14
+
15
+ export const productKeys = {
16
+ all: ['products'] as const,
17
+ lists: () => [...productKeys.all, 'list'] as const,
18
+ list: (filters?: Record<string, unknown>) => [...productKeys.lists(), filters] as const,
19
+ details: () => [...productKeys.all, 'detail'] as const,
20
+ detail: (id: string) => [...productKeys.details(), id] as const,
21
+ bySlug: (spotId: string, slug: string) => [...productKeys.all, 'slug', spotId, slug] as const,
22
+ bySpot: (spotId: string) => [...productKeys.all, 'spot', spotId] as const,
23
+ };
24
+
25
+ // ============================================================================
26
+ // TYPES
27
+ // ============================================================================
28
+
29
+ export interface ProductFilters {
30
+ spotId?: string;
31
+ type?: ProductType;
32
+ status?: ProductStatus;
33
+ limit?: number;
34
+ page?: number;
35
+ }
36
+
37
+ export interface ProductWithSpot extends Product {
38
+ spot: {
39
+ id: string;
40
+ name: string;
41
+ slug: string;
42
+ };
43
+ }
44
+
45
+ // ============================================================================
46
+ // QUERY HOOKS
47
+ // ============================================================================
48
+
49
+ /**
50
+ * Get products for a spot (public browse)
51
+ *
52
+ * @endpoint GET /api/v1/spots/{spotId}/products
53
+ */
54
+ export function useSpotProducts(
55
+ spotId: string,
56
+ params?: { type?: ProductType; limit?: number; page?: number },
57
+ options?: Omit<UseQueryOptions<PaginatedResponse<Product>>, 'queryKey' | 'queryFn'>
58
+ ): UseQueryResult<PaginatedResponse<Product>> {
59
+ return useQuery({
60
+ queryKey: productKeys.bySpot(spotId),
61
+ queryFn: async (): Promise<PaginatedResponse<Product>> => {
62
+ const client = getApiClient();
63
+ const queryParams = new URLSearchParams();
64
+ if (params?.limit) queryParams.set('limit', String(params.limit));
65
+ if (params?.page) queryParams.set('page', String(params.page));
66
+ if (params?.type) queryParams.set('type', params.type);
67
+ const response = await client.get<ApiResponse<PaginatedResponse<Product>>>(
68
+ `/api/v1/spots/${spotId}/products?${queryParams}`
69
+ );
70
+ return response.data.data;
71
+ },
72
+ enabled: !!spotId,
73
+ ...options,
74
+ });
75
+ }
76
+
77
+ /**
78
+ * Get a product by ID
79
+ *
80
+ * @endpoint GET /api/v1/products/{productId}
81
+ */
82
+ export function useProduct(
83
+ productId: string,
84
+ options?: Omit<UseQueryOptions<ProductWithSpot>, 'queryKey' | 'queryFn'>
85
+ ): UseQueryResult<ProductWithSpot> {
86
+ return useQuery({
87
+ queryKey: productKeys.detail(productId),
88
+ queryFn: async (): Promise<ProductWithSpot> => {
89
+ const client = getApiClient();
90
+ const response = await client.get<ApiResponse<ProductWithSpot>>(`/api/v1/products/${productId}`);
91
+ return response.data.data;
92
+ },
93
+ enabled: !!productId,
94
+ ...options,
95
+ });
96
+ }
97
+
98
+ /**
99
+ * Get a product by slug (within a spot)
100
+ *
101
+ * @endpoint GET /api/v1/spots/{spotId}/products/slug/{slug}
102
+ */
103
+ export function useProductBySlug(
104
+ spotId: string,
105
+ slug: string,
106
+ options?: Omit<UseQueryOptions<ProductWithSpot>, 'queryKey' | 'queryFn'>
107
+ ): UseQueryResult<ProductWithSpot> {
108
+ return useQuery({
109
+ queryKey: productKeys.bySlug(spotId, slug),
110
+ queryFn: async (): Promise<ProductWithSpot> => {
111
+ const client = getApiClient();
112
+ const response = await client.get<ApiResponse<ProductWithSpot>>(
113
+ `/api/v1/spots/${spotId}/products/slug/${slug}`
114
+ );
115
+ return response.data.data;
116
+ },
117
+ enabled: !!spotId && !!slug,
118
+ ...options,
119
+ });
120
+ }
121
+
122
+ // Note: To list products as a seller, use useSpotProducts with your spot ID.
123
+ // There is no cross-spot product listing endpoint currently.