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