CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/next/components/store/other-items.tsx
Views: 687
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>("saved-for-later");
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
style={{ width: "100%" }}
71
placeholder="Search..."
72
value={search}
73
onChange={(e) => setSearch(e.target.value)}
74
/>
75
</div>
76
</Col>
77
</Row>
78
<Items
79
onChange={onChange}
80
cart={cart}
81
tab={tab}
82
search={search.toLowerCase()}
83
/>
84
</div>
85
);
86
}
87
88
interface ItemsProps extends Props {
89
tab: Tab;
90
search: string;
91
}
92
93
function Items({ onChange, cart, tab, search }: ItemsProps) {
94
const isMounted = useIsMounted();
95
const [updating, setUpdating] = useState<boolean>(false);
96
const get = useAPI(
97
"/shopping/cart/get",
98
tab == "buy-it-again" ? { purchased: true } : { removed: true },
99
);
100
const items = useMemo(() => {
101
if (!get.result) return undefined;
102
const x: any[] = [];
103
const v = search_split(search);
104
for (const item of get.result) {
105
if (search && !search_match(JSON.stringify(item).toLowerCase(), v))
106
continue;
107
item.cost = computeCost(item.description);
108
x.push(item);
109
}
110
return x;
111
}, [get.result, search]);
112
113
useEffect(() => {
114
get.call();
115
}, [cart.result]);
116
117
if (get.error) {
118
return <Alert type="error" message={get.error} />;
119
}
120
if (get.result == null || items == null) {
121
return <Loading center />;
122
}
123
124
async function reload() {
125
if (!isMounted.current) return;
126
setUpdating(true);
127
try {
128
await get.call();
129
} finally {
130
if (isMounted.current) {
131
setUpdating(false);
132
}
133
}
134
}
135
136
if (items.length == 0) {
137
return (
138
<div style={{ padding: "15px", textAlign: "center", fontSize: "10pt" }}>
139
{tab == "buy-it-again"
140
? `No ${search ? "matching" : ""} previously purchased items.`
141
: `No ${search ? "matching" : ""} items saved for later.`}
142
</div>
143
);
144
}
145
146
const columns = [
147
{
148
responsive: ["xs" as "xs"],
149
render: ({ id, cost, description }) => {
150
return (
151
<div>
152
<DescriptionColumn
153
{...{
154
id,
155
cost,
156
description,
157
updating,
158
setUpdating,
159
isMounted,
160
reload,
161
onChange,
162
tab,
163
}}
164
/>
165
<div>
166
<b style={{ fontSize: "11pt" }}>
167
<DisplayCost cost={cost} simple oneLine />
168
</b>
169
</div>
170
</div>
171
);
172
},
173
},
174
{
175
responsive: ["sm" as "sm"],
176
title: "Product",
177
align: "center" as "center",
178
render: (_, { product }) => <ProductColumn product={product} />,
179
},
180
{
181
responsive: ["sm" as "sm"],
182
width: "60%",
183
render: (_, { id, cost, description }) => (
184
<DescriptionColumn
185
{...{
186
id,
187
cost,
188
description,
189
updating,
190
setUpdating,
191
isMounted,
192
onChange,
193
reload,
194
tab,
195
}}
196
/>
197
),
198
},
199
{
200
responsive: ["sm" as "sm"],
201
title: "Price",
202
align: "right" as "right",
203
render: (_, { cost }) => (
204
<b style={{ fontSize: "11pt" }}>
205
<DisplayCost cost={cost} simple />
206
</b>
207
),
208
},
209
];
210
211
return (
212
<Table
213
showHeader={false}
214
columns={columns}
215
dataSource={items}
216
rowKey={"id"}
217
pagination={{ hideOnSinglePage: true }}
218
/>
219
);
220
}
221
222
function DescriptionColumn({
223
id,
224
cost,
225
description,
226
updating,
227
setUpdating,
228
isMounted,
229
onChange,
230
reload,
231
tab,
232
}) {
233
const { input } = cost;
234
return (
235
<>
236
<div style={{ fontSize: "12pt" }}>
237
{description.title && (
238
<div>
239
<b>{description.title}</b>
240
</div>
241
)}
242
{description.description && <div>{description.description}</div>}
243
{describeItem({ info: input })}
244
</div>
245
<div style={{ marginTop: "5px" }}>
246
<Button
247
disabled={updating}
248
onClick={async () => {
249
setUpdating(true);
250
try {
251
await apiPost("/shopping/cart/add", {
252
id,
253
purchased: tab == "buy-it-again",
254
});
255
if (!isMounted.current) return;
256
onChange();
257
await reload();
258
} finally {
259
if (!isMounted.current) return;
260
setUpdating(false);
261
}
262
}}
263
>
264
<Icon name="shopping-cart" />{" "}
265
{tab == "buy-it-again" ? "Add to Cart" : "Move to Cart"}
266
</Button>
267
{tab == "saved-for-later" && (
268
<Popconfirm
269
title={"Are you sure you want to delete this item?"}
270
onConfirm={async () => {
271
setUpdating(true);
272
try {
273
await apiPost("/shopping/cart/delete", { id });
274
if (!isMounted.current) return;
275
await reload();
276
} finally {
277
if (!isMounted.current) return;
278
setUpdating(false);
279
}
280
}}
281
okText={"Yes, delete this item"}
282
cancelText={"Cancel"}
283
>
284
<Button
285
disabled={updating}
286
type="dashed"
287
style={{ margin: "0 5px" }}
288
>
289
<Icon name="trash" /> Delete
290
</Button>
291
</Popconfirm>
292
)}
293
</div>
294
</>
295
);
296
}
297
298