Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/next/components/store/vouchers.tsx
1450 views
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
/*
7
Voucher -- create vouchers from the contents of your shopping cart.
8
*/
9
10
import { Button, Divider, Form, Input, InputNumber, Radio, Space } from "antd";
11
import { useEffect, useMemo, useState } from "react";
12
import { Icon } from "@cocalc/frontend/components/icon";
13
import { currency, plural } from "@cocalc/util/misc";
14
import A from "components/misc/A";
15
import SiteName from "components/share/site-name";
16
import { useRouter } from "next/router";
17
import { useProfileWithReload } from "lib/hooks/profile";
18
import { Paragraph, Title } from "components/misc";
19
import { RequireEmailAddress } from "./checkout";
20
import ShowError from "@cocalc/frontend/components/error";
21
import vouchers, {
22
CharSet,
23
MAX_VOUCHERS,
24
MAX_VOUCHER_VALUE,
25
WhenPay,
26
} from "@cocalc/util/vouchers";
27
import { ADD_STYLE, AddToCartButton } from "./add-box";
28
import apiPost from "lib/api/post";
29
import Loading from "components/share/loading";
30
31
const STYLE = { color: "#666", fontSize: "12pt" } as const;
32
33
interface Config {
34
whenPay: WhenPay;
35
numVouchers: number;
36
amount: number;
37
length: number;
38
title: string;
39
prefix: string;
40
postfix: string;
41
charset: CharSet;
42
}
43
44
export default function CreateVouchers() {
45
const [form] = Form.useForm();
46
const router = useRouter();
47
const { profile, reload: reloadProfile } = useProfileWithReload({
48
noCache: true,
49
});
50
const [error, setError] = useState<string>("");
51
52
// user configurable options: start
53
const [query, setQuery0] = useState<Config>(() => {
54
const q = router.query;
55
return {
56
whenPay: typeof q.whenPay == "string" ? (q.whenPay as WhenPay) : "now",
57
numVouchers:
58
typeof q.numVouchers == "string" ? parseInt(q.numVouchers) : 1,
59
amount: typeof q.amount == "string" ? parseInt(q.amount) : 5,
60
length: typeof q.length == "string" ? parseInt(q.length) : 8,
61
title: typeof q.title == "string" ? q.title : "CoCalc Voucher Code",
62
prefix: typeof q.prefix == "string" ? q.prefix : "",
63
postfix: typeof q.postfix == "string" ? q.postfix : "",
64
charset: typeof q.charset == "string" ? q.charset : "alphanumeric",
65
};
66
});
67
const {
68
whenPay,
69
numVouchers,
70
amount,
71
length,
72
title,
73
prefix,
74
postfix,
75
charset,
76
} = query;
77
const setQuery = (obj) => {
78
const query1 = { ...query };
79
for (const key in obj) {
80
const value = obj[key];
81
router.query[key] = `${value}`;
82
query1[key] = value;
83
}
84
router.replace({ query: router.query }, undefined, {
85
shallow: true,
86
scroll: false,
87
});
88
setQuery0(query1);
89
};
90
91
const [loading, setLoading] = useState<boolean>(false);
92
useEffect(() => {
93
const { id } = router.query;
94
if (id == null) {
95
return;
96
}
97
// editing something in the shopping cart -- load via an api call
98
(async () => {
99
try {
100
setLoading(true);
101
const item = await apiPost("/shopping/cart/get", { id });
102
if (item.product == "cash-voucher") {
103
const { description } = item;
104
form.setFieldsValue(description);
105
setQuery(description);
106
}
107
} catch (err) {
108
setError(err.message);
109
} finally {
110
setLoading(false);
111
}
112
})();
113
}, []);
114
115
const exampleCodes: string = useMemo(() => {
116
return vouchers({ count: 5, length, charset, prefix, postfix }).join(", ");
117
}, [length, charset, prefix, postfix]);
118
119
// most likely, user will do the purchase and then see the congratulations page
120
useEffect(() => {
121
router.prefetch("/store/congrats");
122
}, []);
123
124
useEffect(() => {
125
if ((numVouchers ?? 0) > MAX_VOUCHERS[whenPay]) {
126
setQuery({ numVouchers: MAX_VOUCHERS[whenPay] });
127
}
128
}, [whenPay]);
129
130
const disabled = !numVouchers || !title?.trim() || !profile?.email_address;
131
132
function renderHeading() {
133
return (
134
<div>
135
<Title level={3}>
136
<Icon name={"gift2"} style={{ marginRight: "5px" }} />{" "}
137
{router.query.id != null
138
? "Edit Voucher in Shopping Cart"
139
: "Configure a Voucher"}
140
</Title>
141
<Paragraph style={STYLE}>
142
Voucher codes are exactly like gift cards. They can be{" "}
143
<A href="/redeem">redeemed</A> by anybody for <SiteName /> credit,
144
which does not expire and can be used to purchase anything on the site
145
(licenses, GPU's, etc.). Visit the{" "}
146
<A href="/vouchers">Voucher Center</A> for more about vouchers, and{" "}
147
<A href="https://doc.cocalc.com/vouchers.html">read the docs</A>. If
148
anything goes wrong with your purchase,{" "}
149
<A href="/support/new">contact support</A> and we will make things
150
right.
151
</Paragraph>
152
</div>
153
);
154
}
155
156
function renderVoucherConfig() {
157
return (
158
<Form layout="horizontal" form={form}>
159
<div>
160
{profile?.is_admin && (
161
<>
162
<h4 style={{ fontSize: "13pt", marginTop: "5px" }}>
163
<Check done /> Admin: Pay or Free
164
</h4>
165
<div>
166
<Form.Item name="whenPay" initialValue={whenPay}>
167
<Radio.Group
168
value={whenPay}
169
onChange={(e) => {
170
setQuery({ whenPay: e.target.value as WhenPay });
171
}}
172
>
173
<Space
174
direction="vertical"
175
style={{ margin: "5px 0 15px 15px" }}
176
>
177
<Radio value={"now"}>Pay</Radio>
178
{profile?.is_admin && (
179
<Radio value={"admin"}>
180
Free: you will not be charged (admins only)
181
</Radio>
182
)}
183
</Space>
184
</Radio.Group>
185
</Form.Item>
186
<br />
187
<Paragraph style={STYLE}>
188
{profile?.is_admin && (
189
<>
190
As an admin, you may select the "Free" option; this is
191
useful for creating free trials, fulfilling complicated
192
customer requirements and adding credit to your own
193
account.
194
</>
195
)}
196
</Paragraph>
197
</div>
198
</>
199
)}
200
<h4 style={{ fontSize: "13pt", marginTop: "20px" }}>
201
<Check done={(numVouchers ?? 0) > 0} /> Value of Each Voucher
202
</h4>
203
<Paragraph style={STYLE}>
204
<div style={{ textAlign: "center" }}>
205
<Form.Item name="amount" initialValue={amount}>
206
<InputNumber
207
size="large"
208
min={1}
209
max={MAX_VOUCHER_VALUE}
210
precision={2} // for two decimal places
211
step={5}
212
value={amount}
213
onChange={(value) => setQuery({ amount: value })}
214
addonBefore="$"
215
/>
216
</Form.Item>
217
</div>
218
</Paragraph>
219
<h4 style={{ fontSize: "13pt", marginTop: "20px" }}>
220
<Check done={(numVouchers ?? 0) > 0} /> Number of Voucher Codes
221
</h4>
222
<Paragraph style={STYLE}>
223
<div style={{ textAlign: "center" }}>
224
<Form.Item name="numVouchers" initialValue={numVouchers}>
225
<InputNumber
226
size="large"
227
style={{ width: "250px" }}
228
min={1}
229
max={MAX_VOUCHERS[whenPay]}
230
value={numVouchers}
231
onChange={(value) => setQuery({ numVouchers: value })}
232
addonAfter={`Voucher ${plural(numVouchers, "Code")}`}
233
/>
234
</Form.Item>
235
</div>
236
</Paragraph>
237
<h4
238
style={{
239
fontSize: "13pt",
240
marginTop: "20px",
241
color: !title ? "darkred" : undefined,
242
}}
243
>
244
<Check done={!!title.trim()} /> Description
245
</h4>
246
<Paragraph style={STYLE}>
247
<div
248
style={
249
!title
250
? { borderRight: "5px solid darkred", paddingRight: "15px" }
251
: undefined
252
}
253
>
254
<Form.Item name="title" initialValue={title}>
255
<Input
256
allowClear
257
style={{ marginTop: "5px", width: "100%" }}
258
onChange={(e) => setQuery({ title: e.target.value })}
259
value={title}
260
addonBefore={"Description"}
261
/>
262
</Form.Item>
263
</div>
264
Customize how your voucher codes are randomly generated (optional):
265
<Space direction="vertical" style={{ marginTop: "5px" }}>
266
<Space style={{ width: "100%" }}>
267
<Form.Item name="length" initialValue={length}>
268
<InputNumber
269
addonBefore={"Length"}
270
min={8}
271
max={16}
272
onChange={(length) => {
273
setQuery({ length: length ?? 8 });
274
}}
275
value={length}
276
/>
277
</Form.Item>
278
<Form.Item name="prefix" initialValue={prefix}>
279
<Input
280
maxLength={10 /* also enforced via api */}
281
onChange={(e) => setQuery({ prefix: e.target.value })}
282
value={prefix}
283
addonBefore={"Prefix"}
284
allowClear
285
/>
286
</Form.Item>
287
<Form.Item name="postfix" initialValue={postfix}>
288
<Input
289
maxLength={10 /* also enforced via api */}
290
onChange={(e) => setQuery({ postfix: e.target.value })}
291
value={postfix}
292
addonBefore={"Postfix"}
293
allowClear
294
/>
295
</Form.Item>
296
</Space>
297
<Form.Item name="charset" initialValue={charset}>
298
<Radio.Group
299
style={{ width: "100%" }}
300
onChange={(e) => {
301
setQuery({ charset: e.target.value });
302
}}
303
defaultValue={charset}
304
>
305
<Radio.Button value="alphanumeric">alphanumeric</Radio.Button>
306
<Radio.Button value="alphabetic">alphabetic</Radio.Button>
307
<Radio.Button value="numbers">0123456789</Radio.Button>
308
<Radio.Button value="lower">lower</Radio.Button>
309
<Radio.Button value="upper">UPPER</Radio.Button>
310
</Radio.Group>
311
</Form.Item>
312
<Space>
313
<div style={{ whiteSpace: "nowrap" }}>
314
Examples (not the actual codes):
315
</div>{" "}
316
{exampleCodes}
317
</Space>
318
</Space>
319
</Paragraph>
320
</div>
321
</Form>
322
);
323
}
324
325
function renderAddBox() {
326
if (query == null) {
327
return null;
328
}
329
const cost = { cost: query.amount * query.numVouchers } as any;
330
return (
331
<div style={{ textAlign: "center" }}>
332
<div style={ADD_STYLE}>
333
<div>
334
<b>{query.title}</b>
335
<br />
336
{numVouchers} voucher {plural(numVouchers, "code")} worth{" "}
337
{currency(amount)} {numVouchers > 1 ? "each" : ""}
338
<br />
339
<Icon name="money-check" /> Total Value: USD {currency(cost.cost)}
340
{whenPay == "admin" && <span> (admin -- no actual charge)</span>}
341
</div>
342
<Divider />
343
<Space>
344
{router.query.id != null && <Button size="large">Cancel</Button>}
345
<AddToCartButton
346
disabled={disabled}
347
cartError={error}
348
cost={cost}
349
form={form}
350
router={router}
351
setCartError={setError}
352
/>
353
</Space>
354
</div>
355
</div>
356
);
357
}
358
359
return (
360
<>
361
{renderHeading()}
362
<RequireEmailAddress profile={profile} reloadProfile={reloadProfile} />
363
<ShowError error={error} setError={setError} />
364
{loading && <Loading large center />}
365
{renderAddBox()}
366
{renderVoucherConfig()}
367
</>
368
);
369
}
370
371
const CHECK_STYLE = { marginRight: "5px", fontSize: "14pt" };
372
function Check({ done }) {
373
if (done) {
374
return <Icon name="check" style={{ ...CHECK_STYLE, color: "green" }} />;
375
} else {
376
return (
377
<Icon name="arrow-right" style={{ ...CHECK_STYLE, color: "#cf1322" }} />
378
);
379
}
380
}
381
382