Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/next/components/store/other-items.tsx
1450 views
1
/*
2
The "Saved for Later" section below the shopping cart.
3
*/
4
5
import { useEffect, useMemo, useState } from "react";
6
import useAPI from "lib/hooks/api";
7
import apiPost from "lib/api/post";
8
import useIsMounted from "lib/hooks/mounted";
9
import {
10
Alert,
11
Button,
12
Input,
13
Menu,
14
MenuProps,
15
Row,
16
Col,
17
Popconfirm,
18
Table,
19
} from "antd";
20
import { DisplayCost, describeItem } from "./site-license-cost";
21
import { computeCost } from "@cocalc/util/licenses/store/compute-cost";
22
import Loading from "components/share/loading";
23
import { Icon } from "@cocalc/frontend/components/icon";
24
import { search_split, search_match } from "@cocalc/util/misc";
25
import { ProductColumn } from "./cart";
26
27
type MenuItem = Required<MenuProps>["items"][number];
28
type Tab = "saved-for-later" | "buy-it-again";
29
30
interface Props {
31
onChange: () => void;
32
cart: { result: any }; // returned by useAPI; used to track when it updates.
33
}
34
35
export default function OtherItems({ onChange, cart }) {
36
const [tab, setTab] = useState<Tab>("buy-it-again");
37
const [search, setSearch] = useState<string>("");
38
39
const items: MenuItem[] = [
40
{ label: "Saved For Later", key: "saved-for-later" as Tab },
41
{ label: "Buy It Again", key: "buy-it-again" as Tab },
42
];
43
44
return (
45
<div>
46
<Row>
47
<Col sm={18} xs={24}>
48
<Menu
49
selectedKeys={[tab]}
50
mode="horizontal"
51
onSelect={(e) => {
52
setTab(e.keyPath[0] as Tab);
53
}}
54
items={items}
55
/>
56
</Col>
57
<Col sm={6}>
58
<div
59
style={{
60
height: "100%",
61
borderBottom: "1px solid #eee" /* hack to match menu */,
62
display: "flex",
63
flexDirection: "column",
64
alignContent: "center",
65
justifyContent: "center",
66
paddingRight: "5px",
67
}}
68
>
69
<Input.Search
70
allowClear
71
style={{ width: "100%" }}
72
placeholder="Search..."
73
value={search}
74
onChange={(e) => setSearch(e.target.value)}
75
/>
76
</div>
77
</Col>
78
</Row>
79
<Items
80
onChange={onChange}
81
cart={cart}
82
tab={tab}
83
search={search.toLowerCase()}
84
/>
85
</div>
86
);
87
}
88
89
interface ItemsProps extends Props {
90
tab: Tab;
91
search: string;
92
}
93
94
function Items({ onChange, cart, tab, search }: ItemsProps) {
95
const isMounted = useIsMounted();
96
const [updating, setUpdating] = useState<boolean>(false);
97
const get = useAPI(
98
"/shopping/cart/get",
99
tab == "buy-it-again" ? { purchased: true } : { removed: true },
100
);
101
const items = useMemo(() => {
102
if (!get.result) {
103
return undefined;
104
}
105
const x: any[] = [];
106
const v = search_split(search);
107
for (const item of get.result) {
108
if (search && !search_match(JSON.stringify(item).toLowerCase(), v)) {
109
continue;
110
}
111
try {
112
item.cost = computeCost(item.description);
113
} catch (_err) {
114
// deprecated, so do not include
115
continue;
116
}
117
x.push(item);
118
}
119
return x;
120
}, [get.result, search]);
121
122
useEffect(() => {
123
get.call();
124
}, [cart.result]);
125
126
if (get.error) {
127
return <Alert type="error" message={get.error} />;
128
}
129
if (get.result == null || items == null) {
130
return <Loading large center />;
131
}
132
133
async function reload() {
134
if (!isMounted.current) return;
135
setUpdating(true);
136
try {
137
await get.call();
138
} finally {
139
if (isMounted.current) {
140
setUpdating(false);
141
}
142
}
143
}
144
145
if (items.length == 0) {
146
return (
147
<div style={{ padding: "15px", textAlign: "center", fontSize: "10pt" }}>
148
{tab == "buy-it-again"
149
? `No ${search ? "matching" : ""} previously purchased items.`
150
: `No ${search ? "matching" : ""} items saved for later.`}
151
</div>
152
);
153
}
154
155
const columns = [
156
{
157
responsive: ["xs" as "xs"],
158
render: ({ id, cost, description }) => {
159
return (
160
<div>
161
<DescriptionColumn
162
{...{
163
id,
164
cost,
165
description,
166
updating,
167
setUpdating,
168
isMounted,
169
reload,
170
onChange,
171
tab,
172
}}
173
/>
174
<div>
175
<b style={{ fontSize: "11pt" }}>
176
<DisplayCost cost={cost} simple oneLine />
177
</b>
178
</div>
179
</div>
180
);
181
},
182
},
183
{
184
responsive: ["sm" as "sm"],
185
title: "Product",
186
align: "center" as "center",
187
render: (_, { product }) => <ProductColumn product={product} />,
188
},
189
{
190
responsive: ["sm" as "sm"],
191
width: "60%",
192
render: (_, { id, cost, description }) => (
193
<DescriptionColumn
194
{...{
195
id,
196
cost,
197
description,
198
updating,
199
setUpdating,
200
isMounted,
201
onChange,
202
reload,
203
tab,
204
}}
205
/>
206
),
207
},
208
{
209
responsive: ["sm" as "sm"],
210
title: "Price",
211
align: "right" as "right",
212
render: (_, { cost }) => (
213
<b style={{ fontSize: "11pt" }}>
214
<DisplayCost cost={cost} simple />
215
</b>
216
),
217
},
218
];
219
220
return (
221
<Table
222
showHeader={false}
223
columns={columns}
224
dataSource={items}
225
rowKey={"id"}
226
pagination={{ hideOnSinglePage: true }}
227
/>
228
);
229
}
230
231
function DescriptionColumn({
232
id,
233
cost,
234
description,
235
updating,
236
setUpdating,
237
isMounted,
238
onChange,
239
reload,
240
tab,
241
}) {
242
const { input } = cost ?? {};
243
return (
244
<>
245
<div style={{ fontSize: "12pt" }}>
246
{description.title && (
247
<div>
248
<b>{description.title}</b>
249
</div>
250
)}
251
{description.description && <div>{description.description}</div>}
252
{input != null && describeItem({ info: input })}
253
</div>
254
<div style={{ marginTop: "5px" }}>
255
<Button
256
disabled={updating}
257
onClick={async () => {
258
setUpdating(true);
259
try {
260
await apiPost("/shopping/cart/add", {
261
id,
262
purchased: tab == "buy-it-again",
263
});
264
if (!isMounted.current) return;
265
onChange();
266
await reload();
267
} finally {
268
if (!isMounted.current) return;
269
setUpdating(false);
270
}
271
}}
272
>
273
<Icon name="shopping-cart" />{" "}
274
{tab == "buy-it-again" ? "Add to Cart" : "Move to Cart"}
275
</Button>
276
{tab == "saved-for-later" && (
277
<Popconfirm
278
title={"Are you sure you want to delete this item?"}
279
onConfirm={async () => {
280
setUpdating(true);
281
try {
282
await apiPost("/shopping/cart/delete", { id });
283
if (!isMounted.current) return;
284
await reload();
285
} finally {
286
if (!isMounted.current) return;
287
setUpdating(false);
288
}
289
}}
290
okText={"Yes, delete this item"}
291
cancelText={"Cancel"}
292
>
293
<Button
294
disabled={updating}
295
type="dashed"
296
style={{ margin: "0 5px" }}
297
>
298
<Icon name="trash" /> Delete
299
</Button>
300
</Popconfirm>
301
)}
302
</div>
303
</>
304
);
305
}
306
307