shelflife-react-hooks 1.0.18 → 1.0.20

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 (44) hide show
  1. package/dist/index.cjs.js +23 -0
  2. package/dist/index.cjs.js.map +1 -1
  3. package/dist/index.d.cts +2 -0
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.esm.js +23 -0
  6. package/dist/index.esm.js.map +1 -1
  7. package/package.json +36 -36
  8. package/src/context/AuthContext.tsx +161 -161
  9. package/src/context/InviteContext.tsx +74 -74
  10. package/src/context/ProductContext.tsx +131 -121
  11. package/src/context/RunningLowContext.tsx +100 -100
  12. package/src/context/ShoppingListContext.tsx +76 -76
  13. package/src/context/StorageContext.tsx +105 -105
  14. package/src/context/StorageItemContext.tsx +157 -157
  15. package/src/context/StorageMemberContext.tsx +84 -84
  16. package/src/context/UserContext.tsx +109 -109
  17. package/src/context/__tests__/contexts.test.tsx +370 -370
  18. package/src/context/api/authApi.ts +155 -155
  19. package/src/context/api/inviteApi.ts +65 -65
  20. package/src/context/api/productApi.ts +223 -201
  21. package/src/context/api/requestState.ts +24 -24
  22. package/src/context/api/runningLowApi.ts +141 -141
  23. package/src/context/api/shoppingListApi.ts +161 -159
  24. package/src/context/api/storageApi.ts +166 -166
  25. package/src/context/api/storageItemApi.ts +260 -260
  26. package/src/context/api/storageMemberApi.ts +84 -84
  27. package/src/context/api/userApi.ts +161 -161
  28. package/src/context/http.ts +22 -22
  29. package/src/index.ts +21 -21
  30. package/src/type/PaginatedResponse.ts +8 -8
  31. package/src/type/auth.ts +79 -79
  32. package/src/type/base.ts +21 -21
  33. package/src/type/item.ts +12 -12
  34. package/src/type/member.ts +6 -6
  35. package/src/type/models.ts +56 -56
  36. package/src/type/product.ts +11 -11
  37. package/src/type/requests.ts +60 -60
  38. package/src/type/runninglow.ts +13 -13
  39. package/src/type/shoppingList.ts +13 -13
  40. package/src/type/storage.ts +7 -7
  41. package/src/type/user.ts +11 -11
  42. package/tsconfig.json +46 -46
  43. package/tsup.config.ts +10 -10
  44. package/vitest.config.ts +8 -8
@@ -1,201 +1,223 @@
1
- import type { Product } from '../../type/models.js';
2
- import type { PaginatedResponse } from '../../type/PaginatedResponse.js';
3
- import type { ProductCreateError } from '../../type/product.js';
4
- import type { CreateProductRequest, UpdateProductRequest } from '../../type/requests.js';
5
- import { buildAuthHeaders, normalizeBaseUrl, readArrayBuffer, readJson } from '../http.js';
6
- import { runWithRequestState, type RequestStateHandlers } from './requestState.js';
7
-
8
- type ProductApiConfig = RequestStateHandlers & {
9
- baseUrl: string;
10
- token: string | null;
11
- setProducts: (value: Product[] | ((items: Product[]) => Product[])) => void;
12
- setProduct: (value: Product | null | ((prev: Product | null) => Product | null)) => void;
13
- };
14
-
15
- const updateById = (items: Product[], updated: Product): Product[] => {
16
- const index = items.findIndex((item) => item.id === updated.id);
17
- if (index === -1) {
18
- return [updated, ...items];
19
- }
20
-
21
- const next = [...items];
22
- next[index] = updated;
23
- return next;
24
- };
25
-
26
- export const fetchProductsRequest = async (
27
- config: ProductApiConfig,
28
- search: string,
29
- size: number,
30
- page: number
31
- ): Promise<PaginatedResponse<Product>> => runWithRequestState(config, async () => {
32
- const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
33
- let url = `${normalizedBaseUrl}/api/products?search=${search}`;
34
- if (size > 0)
35
- url += `&page=${page}&size=${size}`;
36
-
37
- const response = await fetch(url, {
38
- headers: buildAuthHeaders(config.token)
39
- });
40
-
41
- if (!response.ok) {
42
- throw new Error('Failed to fetch products');
43
- }
44
-
45
- const payload = await readJson<PaginatedResponse<Product>>(response);
46
- if (payload) {
47
- config.setProducts(payload.data);
48
- return payload;
49
- }
50
-
51
- return { data: [], currentPage: 0, hasNext: false, hasPrevious: false, pageSize: 0, totalItems: 0, totalPages: 0 };
52
- });
53
-
54
- export const fetchProductRequest = async (
55
- config: ProductApiConfig,
56
- id: number
57
- ): Promise<Product | null> => runWithRequestState(config, async () => {
58
- const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
59
- const response = await fetch(`${normalizedBaseUrl}/api/products/${id}`, {
60
- headers: buildAuthHeaders(config.token)
61
- });
62
-
63
- if (response.status === 404) {
64
- config.setProduct(null);
65
- return null;
66
- }
67
-
68
- if (!response.ok) {
69
- throw new Error('Failed to fetch product');
70
- }
71
-
72
- const payload = await readJson<Product>(response);
73
- if (!payload) {
74
- throw new Error('Product response missing data');
75
- }
76
-
77
- config.setProduct(payload);
78
- config.setProducts((previous) => updateById(previous, payload));
79
- return payload;
80
- });
81
-
82
- export const createProductRequest = async (
83
- config: ProductApiConfig,
84
- dto: CreateProductRequest
85
- ): Promise<Product> => runWithRequestState(config, async () => {
86
- const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
87
- const response = await fetch(`${normalizedBaseUrl}/api/products`, {
88
- method: 'POST',
89
- headers: {
90
- ...buildAuthHeaders(config.token),
91
- 'Content-Type': 'application/json'
92
- },
93
- body: JSON.stringify(dto)
94
- });
95
-
96
- if (!response.ok) {
97
- const err = await readJson<ProductCreateError>(response);
98
-
99
- if (err?.name || err?.barcode || err?.category || err?.expirationDaysDelta)
100
- throw err;
101
-
102
- throw new Error('Failed to create product');
103
- }
104
-
105
- const payload = await readJson<Product>(response);
106
- if (!payload) {
107
- throw new Error('Create product response missing data');
108
- }
109
-
110
- config.setProducts((previous) => [payload, ...previous]);
111
- config.setProduct(payload);
112
- return payload;
113
- });
114
-
115
- export const updateProductRequest = async (
116
- config: ProductApiConfig,
117
- id: number,
118
- dto: UpdateProductRequest
119
- ): Promise<Product> => runWithRequestState(config, async () => {
120
- const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
121
- const response = await fetch(`${normalizedBaseUrl}/api/products/${id}`, {
122
- method: 'PATCH',
123
- headers: {
124
- ...buildAuthHeaders(config.token),
125
- 'Content-Type': 'application/json'
126
- },
127
- body: JSON.stringify(dto)
128
- });
129
-
130
- if (!response.ok) {
131
- const err = await readJson<ProductCreateError>(response);
132
-
133
- if (err?.name || err?.barcode || err?.category || err?.expirationDaysDelta)
134
- throw err;
135
-
136
- throw new Error('Failed to update product');
137
- }
138
-
139
- const payload = await readJson<Product>(response);
140
- if (!payload) {
141
- throw new Error('Update product response missing data');
142
- }
143
-
144
- config.setProducts((previous) => updateById(previous, payload));
145
- config.setProduct(payload);
146
- return payload;
147
- });
148
-
149
- export const deleteProductRequest = async (
150
- config: ProductApiConfig,
151
- id: number
152
- ): Promise<void> => runWithRequestState(config, async () => {
153
- const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
154
- const response = await fetch(`${normalizedBaseUrl}/api/products/${id}`, {
155
- method: 'DELETE',
156
- headers: buildAuthHeaders(config.token)
157
- });
158
-
159
- if (!response.ok) {
160
- throw new Error('Failed to delete product');
161
- }
162
-
163
- config.setProducts((previous) => previous.filter((item) => item.id !== id));
164
- config.setProduct((current) => (current?.id === id ? null : current));
165
- });
166
-
167
- export const getProductIconRequest = async (
168
- config: ProductApiConfig,
169
- id: number
170
- ): Promise<ArrayBuffer> => runWithRequestState(config, async () => {
171
- const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
172
- const response = await fetch(`${normalizedBaseUrl}/api/products/${id}/icon`, {
173
- headers: buildAuthHeaders(config.token)
174
- });
175
-
176
- if (!response.ok) {
177
- throw new Error('Failed to fetch product icon');
178
- }
179
-
180
- return readArrayBuffer(response);
181
- });
182
-
183
- export const uploadProductIconRequest = async (
184
- config: ProductApiConfig,
185
- id: number,
186
- file: Blob
187
- ): Promise<void> => runWithRequestState(config, async () => {
188
- const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
189
- const formData = new FormData();
190
- formData.append('pfp', file);
191
-
192
- const response = await fetch(`${normalizedBaseUrl}/api/products/${id}/icon`, {
193
- method: 'POST',
194
- headers: buildAuthHeaders(config.token),
195
- body: formData
196
- });
197
-
198
- if (!response.ok) {
199
- throw new Error('Failed to upload product icon');
200
- }
201
- });
1
+ import type { Product } from '../../type/models.js';
2
+ import type { PaginatedResponse } from '../../type/PaginatedResponse.js';
3
+ import type { ProductCreateError } from '../../type/product.js';
4
+ import type { CreateProductRequest, UpdateProductRequest } from '../../type/requests.js';
5
+ import { buildAuthHeaders, normalizeBaseUrl, readArrayBuffer, readJson } from '../http.js';
6
+ import { runWithRequestState, type RequestStateHandlers } from './requestState.js';
7
+
8
+ type ProductApiConfig = RequestStateHandlers & {
9
+ baseUrl: string;
10
+ token: string | null;
11
+ setCategories: (value: string[] | ((items: string[]) => string[])) => void;
12
+ setProducts: (value: Product[] | ((items: Product[]) => Product[])) => void;
13
+ setProduct: (value: Product | null | ((prev: Product | null) => Product | null)) => void;
14
+ };
15
+
16
+ const updateById = (items: Product[], updated: Product): Product[] => {
17
+ const index = items.findIndex((item) => item.id === updated.id);
18
+ if (index === -1) {
19
+ return [updated, ...items];
20
+ }
21
+
22
+ const next = [...items];
23
+ next[index] = updated;
24
+ return next;
25
+ };
26
+
27
+ export const fetchProductsRequest = async (
28
+ config: ProductApiConfig,
29
+ search: string,
30
+ size: number,
31
+ page: number
32
+ ): Promise<PaginatedResponse<Product>> => runWithRequestState(config, async () => {
33
+ const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
34
+ let url = `${normalizedBaseUrl}/api/products?search=${search}`;
35
+ if (size > 0)
36
+ url += `&page=${page}&size=${size}`;
37
+
38
+ const response = await fetch(url, {
39
+ headers: buildAuthHeaders(config.token)
40
+ });
41
+
42
+ if (!response.ok) {
43
+ throw new Error('Failed to fetch products');
44
+ }
45
+
46
+ const payload = await readJson<PaginatedResponse<Product>>(response);
47
+ if (payload) {
48
+ config.setProducts(payload.data);
49
+ return payload;
50
+ }
51
+
52
+ return { data: [], currentPage: 0, hasNext: false, hasPrevious: false, pageSize: 0, totalItems: 0, totalPages: 0 };
53
+ });
54
+
55
+ export const fetchProductRequest = async (
56
+ config: ProductApiConfig,
57
+ id: number
58
+ ): Promise<Product | null> => runWithRequestState(config, async () => {
59
+ const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
60
+ const response = await fetch(`${normalizedBaseUrl}/api/products/${id}`, {
61
+ headers: buildAuthHeaders(config.token)
62
+ });
63
+
64
+ if (response.status === 404) {
65
+ config.setProduct(null);
66
+ return null;
67
+ }
68
+
69
+ if (!response.ok) {
70
+ throw new Error('Failed to fetch product');
71
+ }
72
+
73
+ const payload = await readJson<Product>(response);
74
+ if (!payload) {
75
+ throw new Error('Product response missing data');
76
+ }
77
+
78
+ config.setProduct(payload);
79
+ config.setProducts((previous) => updateById(previous, payload));
80
+ return payload;
81
+ });
82
+
83
+ export const fetchCategoriesRequest = async (
84
+ config: ProductApiConfig
85
+ ): Promise<string[] | null> => runWithRequestState(config, async () => {
86
+ const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
87
+ const response = await fetch(`${normalizedBaseUrl}/api/products/categories`, {
88
+ headers: buildAuthHeaders(config.token)
89
+ });
90
+
91
+ if (!response.ok) {
92
+ throw new Error('Failed to fetch categories');
93
+ }
94
+
95
+ const payload = await readJson<string[]>(response);
96
+ if (!payload) {
97
+ throw new Error('Category response missing data');
98
+ }
99
+
100
+ config.setCategories(payload);
101
+ return payload;
102
+ });
103
+
104
+ export const createProductRequest = async (
105
+ config: ProductApiConfig,
106
+ dto: CreateProductRequest
107
+ ): Promise<Product> => runWithRequestState(config, async () => {
108
+ const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
109
+ const response = await fetch(`${normalizedBaseUrl}/api/products`, {
110
+ method: 'POST',
111
+ headers: {
112
+ ...buildAuthHeaders(config.token),
113
+ 'Content-Type': 'application/json'
114
+ },
115
+ body: JSON.stringify(dto)
116
+ });
117
+
118
+ if (!response.ok) {
119
+ const err = await readJson<ProductCreateError>(response);
120
+
121
+ if (err?.name || err?.barcode || err?.category || err?.expirationDaysDelta)
122
+ throw err;
123
+
124
+ throw new Error('Failed to create product');
125
+ }
126
+
127
+ const payload = await readJson<Product>(response);
128
+ if (!payload) {
129
+ throw new Error('Create product response missing data');
130
+ }
131
+
132
+ config.setProducts((previous) => [payload, ...previous]);
133
+ config.setProduct(payload);
134
+ return payload;
135
+ });
136
+
137
+ export const updateProductRequest = async (
138
+ config: ProductApiConfig,
139
+ id: number,
140
+ dto: UpdateProductRequest
141
+ ): Promise<Product> => runWithRequestState(config, async () => {
142
+ const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
143
+ const response = await fetch(`${normalizedBaseUrl}/api/products/${id}`, {
144
+ method: 'PATCH',
145
+ headers: {
146
+ ...buildAuthHeaders(config.token),
147
+ 'Content-Type': 'application/json'
148
+ },
149
+ body: JSON.stringify(dto)
150
+ });
151
+
152
+ if (!response.ok) {
153
+ const err = await readJson<ProductCreateError>(response);
154
+
155
+ if (err?.name || err?.barcode || err?.category || err?.expirationDaysDelta)
156
+ throw err;
157
+
158
+ throw new Error('Failed to update product');
159
+ }
160
+
161
+ const payload = await readJson<Product>(response);
162
+ if (!payload) {
163
+ throw new Error('Update product response missing data');
164
+ }
165
+
166
+ config.setProducts((previous) => updateById(previous, payload));
167
+ config.setProduct(payload);
168
+ return payload;
169
+ });
170
+
171
+ export const deleteProductRequest = async (
172
+ config: ProductApiConfig,
173
+ id: number
174
+ ): Promise<void> => runWithRequestState(config, async () => {
175
+ const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
176
+ const response = await fetch(`${normalizedBaseUrl}/api/products/${id}`, {
177
+ method: 'DELETE',
178
+ headers: buildAuthHeaders(config.token)
179
+ });
180
+
181
+ if (!response.ok) {
182
+ throw new Error('Failed to delete product');
183
+ }
184
+
185
+ config.setProducts((previous) => previous.filter((item) => item.id !== id));
186
+ config.setProduct((current) => (current?.id === id ? null : current));
187
+ });
188
+
189
+ export const getProductIconRequest = async (
190
+ config: ProductApiConfig,
191
+ id: number
192
+ ): Promise<ArrayBuffer> => runWithRequestState(config, async () => {
193
+ const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
194
+ const response = await fetch(`${normalizedBaseUrl}/api/products/${id}/icon`, {
195
+ headers: buildAuthHeaders(config.token)
196
+ });
197
+
198
+ if (!response.ok) {
199
+ throw new Error('Failed to fetch product icon');
200
+ }
201
+
202
+ return readArrayBuffer(response);
203
+ });
204
+
205
+ export const uploadProductIconRequest = async (
206
+ config: ProductApiConfig,
207
+ id: number,
208
+ file: Blob
209
+ ): Promise<void> => runWithRequestState(config, async () => {
210
+ const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
211
+ const formData = new FormData();
212
+ formData.append('pfp', file);
213
+
214
+ const response = await fetch(`${normalizedBaseUrl}/api/products/${id}/icon`, {
215
+ method: 'POST',
216
+ headers: buildAuthHeaders(config.token),
217
+ body: formData
218
+ });
219
+
220
+ if (!response.ok) {
221
+ throw new Error('Failed to upload product icon');
222
+ }
223
+ });
@@ -1,24 +1,24 @@
1
- export type RequestStateHandlers = {
2
- setIsLoading: (value: boolean) => void;
3
- setIsError: (value: boolean) => void;
4
- setError: (error: any | null) => void;
5
- };
6
-
7
- export const runWithRequestState = async <T>(
8
- handlers: RequestStateHandlers,
9
- request: () => Promise<T>
10
- ): Promise<T> => {
11
- handlers.setIsLoading(true);
12
- handlers.setIsError(false);
13
- handlers.setError(null);
14
-
15
- try {
16
- return await request();
17
- } catch (err) {
18
- handlers.setIsError(true);
19
- handlers.setError(err);
20
- throw err;
21
- } finally {
22
- handlers.setIsLoading(false);
23
- }
24
- };
1
+ export type RequestStateHandlers = {
2
+ setIsLoading: (value: boolean) => void;
3
+ setIsError: (value: boolean) => void;
4
+ setError: (error: any | null) => void;
5
+ };
6
+
7
+ export const runWithRequestState = async <T>(
8
+ handlers: RequestStateHandlers,
9
+ request: () => Promise<T>
10
+ ): Promise<T> => {
11
+ handlers.setIsLoading(true);
12
+ handlers.setIsError(false);
13
+ handlers.setError(null);
14
+
15
+ try {
16
+ return await request();
17
+ } catch (err) {
18
+ handlers.setIsError(true);
19
+ handlers.setError(err);
20
+ throw err;
21
+ } finally {
22
+ handlers.setIsLoading(false);
23
+ }
24
+ };