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.
- package/dist/index.cjs.js +23 -0
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.esm.js +23 -0
- package/dist/index.esm.js.map +1 -1
- package/package.json +36 -36
- package/src/context/AuthContext.tsx +161 -161
- package/src/context/InviteContext.tsx +74 -74
- package/src/context/ProductContext.tsx +131 -121
- package/src/context/RunningLowContext.tsx +100 -100
- package/src/context/ShoppingListContext.tsx +76 -76
- package/src/context/StorageContext.tsx +105 -105
- package/src/context/StorageItemContext.tsx +157 -157
- package/src/context/StorageMemberContext.tsx +84 -84
- package/src/context/UserContext.tsx +109 -109
- package/src/context/__tests__/contexts.test.tsx +370 -370
- package/src/context/api/authApi.ts +155 -155
- package/src/context/api/inviteApi.ts +65 -65
- package/src/context/api/productApi.ts +223 -201
- package/src/context/api/requestState.ts +24 -24
- package/src/context/api/runningLowApi.ts +141 -141
- package/src/context/api/shoppingListApi.ts +161 -159
- package/src/context/api/storageApi.ts +166 -166
- package/src/context/api/storageItemApi.ts +260 -260
- package/src/context/api/storageMemberApi.ts +84 -84
- package/src/context/api/userApi.ts +161 -161
- package/src/context/http.ts +22 -22
- package/src/index.ts +21 -21
- package/src/type/PaginatedResponse.ts +8 -8
- package/src/type/auth.ts +79 -79
- package/src/type/base.ts +21 -21
- package/src/type/item.ts +12 -12
- package/src/type/member.ts +6 -6
- package/src/type/models.ts +56 -56
- package/src/type/product.ts +11 -11
- package/src/type/requests.ts +60 -60
- package/src/type/runninglow.ts +13 -13
- package/src/type/shoppingList.ts +13 -13
- package/src/type/storage.ts +7 -7
- package/src/type/user.ts +11 -11
- package/tsconfig.json +46 -46
- package/tsup.config.ts +10 -10
- package/vitest.config.ts +8 -8
|
@@ -1,155 +1,155 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ChangePasswordErrorResponse,
|
|
3
|
-
ChangePasswordRequest,
|
|
4
|
-
LoginErrorResponse,
|
|
5
|
-
LoginRequest,
|
|
6
|
-
LoginResponse,
|
|
7
|
-
SignupError,
|
|
8
|
-
SignupRequest,
|
|
9
|
-
User
|
|
10
|
-
} from '../../type/auth.js';
|
|
11
|
-
import type { ErrorResponse } from '../../type/base.js';
|
|
12
|
-
import { buildAuthHeaders, normalizeBaseUrl, readJson } from '../http.js';
|
|
13
|
-
import { runWithRequestState, type RequestStateHandlers } from './requestState.js';
|
|
14
|
-
|
|
15
|
-
type AuthApiConfig = RequestStateHandlers & {
|
|
16
|
-
baseUrl: string;
|
|
17
|
-
token: string | null;
|
|
18
|
-
setToken: (token: string | null) => void;
|
|
19
|
-
setUser: (user: User | null) => void;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export const loginRequest = async (
|
|
23
|
-
config: AuthApiConfig,
|
|
24
|
-
dto: LoginRequest
|
|
25
|
-
): Promise<void> => runWithRequestState(config, async () => {
|
|
26
|
-
const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
|
|
27
|
-
const response = await fetch(`${normalizedBaseUrl}/api/auth/login`, {
|
|
28
|
-
method: 'POST',
|
|
29
|
-
headers: {
|
|
30
|
-
'Content-Type': 'application/json'
|
|
31
|
-
},
|
|
32
|
-
body: JSON.stringify(dto)
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
if (!response.ok) {
|
|
36
|
-
const error = await readJson<LoginErrorResponse>(response);
|
|
37
|
-
|
|
38
|
-
if (error?.error)
|
|
39
|
-
throw error;
|
|
40
|
-
|
|
41
|
-
throw new Error('Login failed');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const payload = await readJson<LoginResponse>(response);
|
|
45
|
-
if (!payload || !('token' in payload)) {
|
|
46
|
-
throw new Error('Login response missing token');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
config.setToken(payload.token);
|
|
50
|
-
|
|
51
|
-
const meResponse = await fetch(`${normalizedBaseUrl}/api/auth/me`, {
|
|
52
|
-
headers: buildAuthHeaders(payload.token)
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
if (meResponse.ok) {
|
|
56
|
-
const mePayload = await readJson<User>(meResponse);
|
|
57
|
-
if (mePayload) {
|
|
58
|
-
config.setUser(mePayload);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
export const signupRequest = async (
|
|
64
|
-
config: AuthApiConfig,
|
|
65
|
-
dto: SignupRequest
|
|
66
|
-
): Promise<User> => runWithRequestState(config, async () => {
|
|
67
|
-
const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
|
|
68
|
-
const response = await fetch(`${normalizedBaseUrl}/api/auth/signup`, {
|
|
69
|
-
method: 'POST',
|
|
70
|
-
headers: {
|
|
71
|
-
'Content-Type': 'application/json'
|
|
72
|
-
},
|
|
73
|
-
body: JSON.stringify(dto)
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
if (!response.ok) {
|
|
77
|
-
const error = await readJson<SignupError>(response);
|
|
78
|
-
|
|
79
|
-
if (error?.email || error?.password || error?.passwordRepeat || error?.username )
|
|
80
|
-
throw error;
|
|
81
|
-
|
|
82
|
-
throw new Error('Signup failed');
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const payload = await readJson<User>(response);
|
|
86
|
-
if (!payload) {
|
|
87
|
-
throw new Error('Signup response missing user');
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return payload;
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
export const changePasswordRequest = async (
|
|
94
|
-
config: AuthApiConfig,
|
|
95
|
-
dto: ChangePasswordRequest
|
|
96
|
-
): Promise<void> => runWithRequestState(config, async () => {
|
|
97
|
-
const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
|
|
98
|
-
const response = await fetch(`${normalizedBaseUrl}/api/auth/password/change`, {
|
|
99
|
-
method: 'POST',
|
|
100
|
-
headers: {
|
|
101
|
-
...buildAuthHeaders(config.token),
|
|
102
|
-
'Content-Type': 'application/json'
|
|
103
|
-
},
|
|
104
|
-
body: JSON.stringify(dto)
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
if (!response.ok) {
|
|
108
|
-
const error = await readJson<ChangePasswordErrorResponse>(response);
|
|
109
|
-
|
|
110
|
-
if (error?.oldPassword || error?.newPassword || error?.newPasswordRepeat)
|
|
111
|
-
throw error;
|
|
112
|
-
|
|
113
|
-
throw new Error('Change password failed');
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
export const getMeRequest = async (config: AuthApiConfig): Promise<User | null> => runWithRequestState(
|
|
118
|
-
config,
|
|
119
|
-
async () => {
|
|
120
|
-
const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
|
|
121
|
-
const response = await fetch(`${normalizedBaseUrl}/api/auth/me`, {
|
|
122
|
-
headers: buildAuthHeaders(config.token)
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
if (!response.ok) {
|
|
126
|
-
config.setUser(null);
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const payload = await readJson<User>(response);
|
|
131
|
-
if (!payload) {
|
|
132
|
-
config.setUser(null);
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
config.setUser(payload);
|
|
137
|
-
return payload;
|
|
138
|
-
}
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
export const logoutRequest = async (config: AuthApiConfig): Promise<void> => runWithRequestState(
|
|
142
|
-
config,
|
|
143
|
-
async () => {
|
|
144
|
-
const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
|
|
145
|
-
if (config.token) {
|
|
146
|
-
await fetch(`${normalizedBaseUrl}/api/auth/logout`, {
|
|
147
|
-
method: 'POST',
|
|
148
|
-
headers: buildAuthHeaders(config.token)
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
config.setToken(null);
|
|
153
|
-
config.setUser(null);
|
|
154
|
-
}
|
|
155
|
-
);
|
|
1
|
+
import type {
|
|
2
|
+
ChangePasswordErrorResponse,
|
|
3
|
+
ChangePasswordRequest,
|
|
4
|
+
LoginErrorResponse,
|
|
5
|
+
LoginRequest,
|
|
6
|
+
LoginResponse,
|
|
7
|
+
SignupError,
|
|
8
|
+
SignupRequest,
|
|
9
|
+
User
|
|
10
|
+
} from '../../type/auth.js';
|
|
11
|
+
import type { ErrorResponse } from '../../type/base.js';
|
|
12
|
+
import { buildAuthHeaders, normalizeBaseUrl, readJson } from '../http.js';
|
|
13
|
+
import { runWithRequestState, type RequestStateHandlers } from './requestState.js';
|
|
14
|
+
|
|
15
|
+
type AuthApiConfig = RequestStateHandlers & {
|
|
16
|
+
baseUrl: string;
|
|
17
|
+
token: string | null;
|
|
18
|
+
setToken: (token: string | null) => void;
|
|
19
|
+
setUser: (user: User | null) => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const loginRequest = async (
|
|
23
|
+
config: AuthApiConfig,
|
|
24
|
+
dto: LoginRequest
|
|
25
|
+
): Promise<void> => runWithRequestState(config, async () => {
|
|
26
|
+
const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
|
|
27
|
+
const response = await fetch(`${normalizedBaseUrl}/api/auth/login`, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: {
|
|
30
|
+
'Content-Type': 'application/json'
|
|
31
|
+
},
|
|
32
|
+
body: JSON.stringify(dto)
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
const error = await readJson<LoginErrorResponse>(response);
|
|
37
|
+
|
|
38
|
+
if (error?.error)
|
|
39
|
+
throw error;
|
|
40
|
+
|
|
41
|
+
throw new Error('Login failed');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const payload = await readJson<LoginResponse>(response);
|
|
45
|
+
if (!payload || !('token' in payload)) {
|
|
46
|
+
throw new Error('Login response missing token');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
config.setToken(payload.token);
|
|
50
|
+
|
|
51
|
+
const meResponse = await fetch(`${normalizedBaseUrl}/api/auth/me`, {
|
|
52
|
+
headers: buildAuthHeaders(payload.token)
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (meResponse.ok) {
|
|
56
|
+
const mePayload = await readJson<User>(meResponse);
|
|
57
|
+
if (mePayload) {
|
|
58
|
+
config.setUser(mePayload);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export const signupRequest = async (
|
|
64
|
+
config: AuthApiConfig,
|
|
65
|
+
dto: SignupRequest
|
|
66
|
+
): Promise<User> => runWithRequestState(config, async () => {
|
|
67
|
+
const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
|
|
68
|
+
const response = await fetch(`${normalizedBaseUrl}/api/auth/signup`, {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: {
|
|
71
|
+
'Content-Type': 'application/json'
|
|
72
|
+
},
|
|
73
|
+
body: JSON.stringify(dto)
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
const error = await readJson<SignupError>(response);
|
|
78
|
+
|
|
79
|
+
if (error?.email || error?.password || error?.passwordRepeat || error?.username )
|
|
80
|
+
throw error;
|
|
81
|
+
|
|
82
|
+
throw new Error('Signup failed');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const payload = await readJson<User>(response);
|
|
86
|
+
if (!payload) {
|
|
87
|
+
throw new Error('Signup response missing user');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return payload;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export const changePasswordRequest = async (
|
|
94
|
+
config: AuthApiConfig,
|
|
95
|
+
dto: ChangePasswordRequest
|
|
96
|
+
): Promise<void> => runWithRequestState(config, async () => {
|
|
97
|
+
const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
|
|
98
|
+
const response = await fetch(`${normalizedBaseUrl}/api/auth/password/change`, {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
headers: {
|
|
101
|
+
...buildAuthHeaders(config.token),
|
|
102
|
+
'Content-Type': 'application/json'
|
|
103
|
+
},
|
|
104
|
+
body: JSON.stringify(dto)
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
const error = await readJson<ChangePasswordErrorResponse>(response);
|
|
109
|
+
|
|
110
|
+
if (error?.oldPassword || error?.newPassword || error?.newPasswordRepeat)
|
|
111
|
+
throw error;
|
|
112
|
+
|
|
113
|
+
throw new Error('Change password failed');
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
export const getMeRequest = async (config: AuthApiConfig): Promise<User | null> => runWithRequestState(
|
|
118
|
+
config,
|
|
119
|
+
async () => {
|
|
120
|
+
const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
|
|
121
|
+
const response = await fetch(`${normalizedBaseUrl}/api/auth/me`, {
|
|
122
|
+
headers: buildAuthHeaders(config.token)
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
config.setUser(null);
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const payload = await readJson<User>(response);
|
|
131
|
+
if (!payload) {
|
|
132
|
+
config.setUser(null);
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
config.setUser(payload);
|
|
137
|
+
return payload;
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
export const logoutRequest = async (config: AuthApiConfig): Promise<void> => runWithRequestState(
|
|
142
|
+
config,
|
|
143
|
+
async () => {
|
|
144
|
+
const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
|
|
145
|
+
if (config.token) {
|
|
146
|
+
await fetch(`${normalizedBaseUrl}/api/auth/logout`, {
|
|
147
|
+
method: 'POST',
|
|
148
|
+
headers: buildAuthHeaders(config.token)
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
config.setToken(null);
|
|
153
|
+
config.setUser(null);
|
|
154
|
+
}
|
|
155
|
+
);
|
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
import type { StorageMember } from '../../type/models.js';
|
|
2
|
-
import { buildAuthHeaders, normalizeBaseUrl, readJson } from '../http.js';
|
|
3
|
-
import { runWithRequestState, type RequestStateHandlers } from './requestState.js';
|
|
4
|
-
|
|
5
|
-
type InviteApiConfig = RequestStateHandlers & {
|
|
6
|
-
baseUrl: string;
|
|
7
|
-
token: string | null;
|
|
8
|
-
setInvites: (value: StorageMember[] | ((items: StorageMember[]) => StorageMember[])) => void;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export const fetchInvitesRequest = async (config: InviteApiConfig): Promise<StorageMember[]> => runWithRequestState(
|
|
12
|
-
config,
|
|
13
|
-
async () => {
|
|
14
|
-
const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
|
|
15
|
-
const response = await fetch(`${normalizedBaseUrl}/api/storages/invites`, {
|
|
16
|
-
headers: buildAuthHeaders(config.token)
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
if (!response.ok) {
|
|
20
|
-
throw new Error('Failed to fetch invites');
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const payload = await readJson<StorageMember[]>(response);
|
|
24
|
-
if (payload) {
|
|
25
|
-
config.setInvites(payload);
|
|
26
|
-
return payload;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return [];
|
|
30
|
-
}
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
export const acceptInviteRequest = async (
|
|
34
|
-
config: InviteApiConfig,
|
|
35
|
-
id: number
|
|
36
|
-
): Promise<void> => runWithRequestState(config, async () => {
|
|
37
|
-
const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
|
|
38
|
-
const response = await fetch(`${normalizedBaseUrl}/api/storages/invites/${id}`, {
|
|
39
|
-
method: 'POST',
|
|
40
|
-
headers: buildAuthHeaders(config.token)
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
if (!response.ok) {
|
|
44
|
-
throw new Error('Failed to accept invite');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
config.setInvites((previous) => previous.filter((invite) => invite.id !== id));
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
export const declineInviteRequest = async (
|
|
51
|
-
config: InviteApiConfig,
|
|
52
|
-
id: number
|
|
53
|
-
): Promise<void> => runWithRequestState(config, async () => {
|
|
54
|
-
const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
|
|
55
|
-
const response = await fetch(`${normalizedBaseUrl}/api/storages/invites/${id}`, {
|
|
56
|
-
method: 'DELETE',
|
|
57
|
-
headers: buildAuthHeaders(config.token)
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
if (!response.ok) {
|
|
61
|
-
throw new Error('Failed to decline invite');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
config.setInvites((previous) => previous.filter((invite) => invite.id !== id));
|
|
65
|
-
});
|
|
1
|
+
import type { StorageMember } from '../../type/models.js';
|
|
2
|
+
import { buildAuthHeaders, normalizeBaseUrl, readJson } from '../http.js';
|
|
3
|
+
import { runWithRequestState, type RequestStateHandlers } from './requestState.js';
|
|
4
|
+
|
|
5
|
+
type InviteApiConfig = RequestStateHandlers & {
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
token: string | null;
|
|
8
|
+
setInvites: (value: StorageMember[] | ((items: StorageMember[]) => StorageMember[])) => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const fetchInvitesRequest = async (config: InviteApiConfig): Promise<StorageMember[]> => runWithRequestState(
|
|
12
|
+
config,
|
|
13
|
+
async () => {
|
|
14
|
+
const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
|
|
15
|
+
const response = await fetch(`${normalizedBaseUrl}/api/storages/invites`, {
|
|
16
|
+
headers: buildAuthHeaders(config.token)
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
throw new Error('Failed to fetch invites');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const payload = await readJson<StorageMember[]>(response);
|
|
24
|
+
if (payload) {
|
|
25
|
+
config.setInvites(payload);
|
|
26
|
+
return payload;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
export const acceptInviteRequest = async (
|
|
34
|
+
config: InviteApiConfig,
|
|
35
|
+
id: number
|
|
36
|
+
): Promise<void> => runWithRequestState(config, async () => {
|
|
37
|
+
const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
|
|
38
|
+
const response = await fetch(`${normalizedBaseUrl}/api/storages/invites/${id}`, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: buildAuthHeaders(config.token)
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
throw new Error('Failed to accept invite');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
config.setInvites((previous) => previous.filter((invite) => invite.id !== id));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const declineInviteRequest = async (
|
|
51
|
+
config: InviteApiConfig,
|
|
52
|
+
id: number
|
|
53
|
+
): Promise<void> => runWithRequestState(config, async () => {
|
|
54
|
+
const normalizedBaseUrl = normalizeBaseUrl(config.baseUrl);
|
|
55
|
+
const response = await fetch(`${normalizedBaseUrl}/api/storages/invites/${id}`, {
|
|
56
|
+
method: 'DELETE',
|
|
57
|
+
headers: buildAuthHeaders(config.token)
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
throw new Error('Failed to decline invite');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
config.setInvites((previous) => previous.filter((invite) => invite.id !== id));
|
|
65
|
+
});
|