Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/next/components/store/add-box.tsx
5903 views
1
/*
2
* This file is part of CoCalc: Copyright © 2022 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
/*
7
Add a cash voucher to your shopping cart.
8
*/
9
import { useState, type JSX } from "react";
10
import { Alert, Button, Spin } from "antd";
11
import { CostInputPeriod } from "@cocalc/util/licenses/purchase/types";
12
import { round2up } from "@cocalc/util/misc";
13
import { money } from "@cocalc/util/licenses/purchase/utils";
14
import { addToCart } from "./add-to-cart";
15
import { DisplayCost } from "./site-license-cost";
16
import { periodicCost } from "@cocalc/util/licenses/purchase/compute-cost";
17
import { decimalDivide } from "@cocalc/util/stripe/calc";
18
import ShowError from "@cocalc/frontend/components/error";
19
import type { LicenseSource } from "@cocalc/util/upgrades/shopping";
20
21
export const ADD_STYLE = {
22
display: "inline-block",
23
maxWidth: "550px",
24
minWidth: "400px",
25
background: "#fafafa",
26
border: "1px solid #ccc",
27
padding: "10px 20px",
28
borderRadius: "5px",
29
margin: "15px 0",
30
fontSize: "12pt",
31
} as const;
32
33
interface Props {
34
cost?: CostInputPeriod;
35
router;
36
form;
37
cartError: string | undefined;
38
setCartError: (error) => void;
39
dedicatedItem?: boolean;
40
disabled?: boolean;
41
noAccount: boolean;
42
source: LicenseSource;
43
}
44
45
export function AddBox({
46
cost,
47
router,
48
form,
49
cartError,
50
setCartError,
51
dedicatedItem = false,
52
noAccount,
53
disabled = false,
54
source,
55
}: Props) {
56
if (cost?.input.type == "cash-voucher") {
57
return null;
58
}
59
// if any of the fields in cost that start with the string "cost" are NaN, disable submission:
60
if (
61
!cost ||
62
Object.keys(cost).some((k) => k.startsWith("cost") && isNaN(cost[k]))
63
) {
64
disabled = true;
65
}
66
67
function costPerProject() {
68
if (cost?.input.type != "quota") {
69
return;
70
}
71
if (dedicatedItem || cost.input.quantity == null) {
72
return;
73
}
74
const costPer = decimalDivide(periodicCost(cost), cost.input.quantity);
75
return (
76
<Alert
77
type="warning"
78
style={{
79
margin: "10px",
80
}}
81
message={
82
<>
83
{money(round2up(costPer))}{" "}
84
<b>per {source === "course" ? "student" : "project"}</b>{" "}
85
{!!cost.period && cost.period != "range" ? cost.period : ""}
86
</>
87
}
88
/>
89
);
90
}
91
92
function renderButton(): JSX.Element | null {
93
if (noAccount) return null;
94
95
return (
96
<div style={{ textAlign: "center", marginTop: "5px" }}>
97
{router.query.id != null && (
98
<Button
99
size="large"
100
style={{ marginRight: "5px" }}
101
onClick={() => router.push("/store/cart")}
102
disabled={disabled}
103
>
104
Cancel
105
</Button>
106
)}
107
<AddToCartButton
108
cartError={cartError}
109
cost={cost}
110
disabled={disabled}
111
form={form}
112
router={router}
113
setCartError={setCartError}
114
/>
115
<ShowError
116
error={cartError}
117
setError={setCartError}
118
style={{ marginTop: "5px" }}
119
/>
120
</div>
121
);
122
}
123
124
return (
125
<div style={{ textAlign: "center" }}>
126
<div style={ADD_STYLE}>
127
{cost && <DisplayCost cost={cost} />}
128
{cost && costPerProject()}
129
{renderButton()}
130
</div>
131
</div>
132
);
133
}
134
135
interface CartButtonProps {
136
cost: CostInputPeriod | undefined;
137
router;
138
form;
139
setCartError: (error) => void;
140
disabled?: boolean;
141
cartError: string | undefined;
142
variant?: "primary" | "small";
143
}
144
145
export function AddToCartButton({
146
cost,
147
form,
148
router,
149
setCartError,
150
cartError,
151
variant = "primary",
152
disabled: disabled0,
153
}: CartButtonProps) {
154
const [clicked, setClicked] = useState<boolean>(false);
155
const disabled =
156
clicked ||
157
(disabled0 ?? false) ||
158
!!cartError ||
159
cost == null ||
160
cost.cost === 0;
161
162
return (
163
<Button
164
size={variant === "small" ? "small" : "large"}
165
type="primary"
166
htmlType="submit"
167
onClick={async () => {
168
// you can only click this add to cart button *once* -- due to slow
169
// turnaround, if we don't change this state, then the user could
170
// click multiple times and add the same item more than once, thus
171
// accidentally ending up with a "dobule purchase"
172
try {
173
setClicked(true);
174
await addToCart({ form, setCartError, router });
175
} catch (_err) {
176
// error is reported via setCartError. But also
177
// give a chance to click the button again, since item
178
// wasn't actually added.
179
setClicked(false);
180
}
181
}}
182
disabled={disabled}
183
>
184
{clicked
185
? "Moving to Cart..."
186
: router.query.id != null
187
? "Save Changes"
188
: "Add to Cart"}
189
{clicked && <Spin style={{ marginLeft: "15px" }} />}
190
</Button>
191
);
192
}
193
194