Skip to content

Commit ffc5f8c

Browse files
authored
Merge pull request #8437 from sagemathinc/store-purchase-course
next/store: "course" purchases
2 parents 2fe6d18 + cd4c739 commit ffc5f8c

File tree

27 files changed

+997
-389
lines changed

27 files changed

+997
-389
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,5 +160,7 @@ src/packages/frontend/i18n/trans/*.compiled.json
160160
**/project-env.sh
161161
**/*.bash_history
162162

163+
src/.claude/settings.local.json
164+
163165
# test reports by jest-junit
164166
junit.xml
Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
{
22
"permissions": {
33
"allow": [
4-
"Bash(pnpm tsc:*)",
5-
"Bash(pnpm build:*)",
4+
"Bash(find:*)",
5+
"Bash(gh pr view:*)",
6+
"Bash(gh:*)",
67
"Bash(git add:*)",
78
"Bash(git commit:*)",
8-
"Bash(node:*)",
99
"Bash(grep:*)",
10-
"Bash(find:*)",
11-
"WebFetch(domain:github.com)",
12-
"WebFetch(domain:cocalc.com)",
13-
"WebFetch(domain:doc.cocalc.com)",
10+
"Bash(node:*)",
1411
"Bash(npm show:*)",
15-
"Bash(prettier -w:*)",
1612
"Bash(npx tsc:*)",
17-
"Bash(gh pr view:*)",
18-
"Bash(gh:*)"
13+
"Bash(pnpm build:*)",
14+
"Bash(pnpm ts-build:*)",
15+
"Bash(pnpm tsc:*)",
16+
"Bash(prettier -w:*)",
17+
"WebFetch(domain:cocalc.com)",
18+
"WebFetch(domain:doc.cocalc.com)",
19+
"WebFetch(domain:docs.anthropic.com)",
20+
"WebFetch(domain:github.com)"
1921
],
2022
"deny": []
2123
}
22-
}
24+
}

src/packages/frontend/components/help-icon.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ Display a ? "help" icon, which -- when clicked -- shows a help tip
99

1010
import { Button, Popover } from "antd";
1111
import type { TooltipPlacement } from "antd/es/tooltip";
12-
import { CSSProperties } from "react";
12+
import { CSSProperties, useState } from "react";
1313

14-
import { CSS, React, useState } from "@cocalc/frontend/app-framework";
14+
// ATTN: do not import @cocalc/app-framework or components, because this is also used in next!
1515
import { COLORS } from "@cocalc/util/theme";
1616
import { Icon } from "./icon";
1717

@@ -34,7 +34,7 @@ export const HelpIcon: React.FC<Props> = ({
3434
}: Props) => {
3535
const [open, setOpen] = useState<boolean>(false);
3636

37-
const textStyle: CSS = {
37+
const textStyle: CSSProperties = {
3838
color: COLORS.BS_BLUE_TEXT,
3939
cursor: "pointer",
4040
...style,
@@ -68,8 +68,8 @@ export const HelpIcon: React.FC<Props> = ({
6868
onOpenChange={setOpen}
6969
>
7070
<span style={textStyle}>
71+
{extra ? <>{extra} </> : undefined}
7172
<Icon style={textStyle} name="question-circle" />
72-
{extra ? <> {extra}</> : undefined}
7373
</span>
7474
</Popover>
7575
);

src/packages/next/components/store/add-box.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,18 @@
77
Add a cash voucher to your shopping cart.
88
*/
99
import { useState, type JSX } from "react";
10+
import { Alert, Button, Spin } from "antd";
11+
1012
import { CostInputPeriod } from "@cocalc/util/licenses/purchase/types";
1113
import { round2up } from "@cocalc/util/misc";
1214
import { money } from "@cocalc/util/licenses/purchase/utils";
13-
import { Alert, Button, Spin } from "antd";
1415
import { addToCart } from "./add-to-cart";
1516
import { DisplayCost } from "./site-license-cost";
1617
import { periodicCost } from "@cocalc/util/licenses/purchase/compute-cost";
1718
import { decimalDivide } from "@cocalc/util/stripe/calc";
1819

20+
import type { LicenseSource } from "@cocalc/util/upgrades/shopping";
21+
1922
export const ADD_STYLE = {
2023
display: "inline-block",
2124
maxWidth: "550px",
@@ -37,6 +40,7 @@ interface Props {
3740
dedicatedItem?: boolean;
3841
disabled?: boolean;
3942
noAccount: boolean;
43+
source: LicenseSource;
4044
}
4145

4246
export function AddBox({
@@ -48,6 +52,7 @@ export function AddBox({
4852
dedicatedItem = false,
4953
noAccount,
5054
disabled = false,
55+
source,
5156
}: Props) {
5257
if (cost?.input.type == "cash-voucher") {
5358
return null;
@@ -76,7 +81,8 @@ export function AddBox({
7681
}}
7782
message={
7883
<>
79-
{money(round2up(costPer))} <b>per project</b>{" "}
84+
{money(round2up(costPer))}{" "}
85+
<b>per {source === "course" ? "student" : "project"}</b>{" "}
8086
{!!cost.period && cost.period != "range" ? cost.period : ""}
8187
</>
8288
}
@@ -175,8 +181,8 @@ export function AddToCartButton({
175181
{clicked
176182
? "Moving to Cart..."
177183
: router.query.id != null
178-
? "Save Changes"
179-
: "Add to Cart"}
184+
? "Save Changes"
185+
: "Add to Cart"}
180186
{clicked && <Spin style={{ marginLeft: "15px" }} />}
181187
</Button>
182188
);

src/packages/next/components/store/cart.tsx

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,27 @@ shopping cart experience, so most likely to feel familiar to users and easy
1111
to use.
1212
*/
1313

14+
import { Alert, Button, Checkbox, Popconfirm, Table } from "antd";
15+
import { useRouter } from "next/router";
16+
import { useEffect, useMemo, useState, type JSX } from "react";
17+
1418
import { Icon } from "@cocalc/frontend/components/icon";
19+
import type {
20+
ProductDescription,
21+
ProductType,
22+
} from "@cocalc/util/db-schema/shopping-cart-items";
1523
import { describeQuotaFromInfo } from "@cocalc/util/licenses/describe-quota";
1624
import { CostInputPeriod } from "@cocalc/util/licenses/purchase/types";
25+
import { computeCost } from "@cocalc/util/licenses/store/compute-cost";
1726
import { capitalize, isValidUUID } from "@cocalc/util/misc";
18-
import { Alert, Button, Checkbox, Popconfirm, Table } from "antd";
1927
import A from "components/misc/A";
2028
import Loading from "components/share/loading";
2129
import SiteName from "components/share/site-name";
2230
import apiPost from "lib/api/post";
2331
import useAPI from "lib/hooks/api";
2432
import useIsMounted from "lib/hooks/mounted";
25-
import { useRouter } from "next/router";
26-
import { useEffect, useMemo, useState, type JSX } from "react";
27-
import { computeCost } from "@cocalc/util/licenses/store/compute-cost";
2833
import OtherItems from "./other-items";
2934
import { describeItem, describePeriod, DisplayCost } from "./site-license-cost";
30-
import type {
31-
ProductDescription,
32-
ProductType,
33-
} from "@cocalc/util/db-schema/shopping-cart-items";
3435

3536
export default function ShoppingCart() {
3637
const isMounted = useIsMounted();
@@ -353,9 +354,9 @@ export function DescriptionColumn(props: DCProps) {
353354
const router = useRouter();
354355
const { id, description, style, readOnly } = props;
355356
if (
356-
description.type == "disk" ||
357-
description.type == "vm" ||
358-
description.type == "quota"
357+
description.type === "disk" ||
358+
description.type === "vm" ||
359+
description.type === "quota"
359360
) {
360361
return <DescriptionColumnSiteLicense {...props} />;
361362
} else if (description.type == "cash-voucher") {
@@ -390,9 +391,9 @@ function DescriptionColumnSiteLicense(props: DCProps) {
390391
const { id, cost, description, compact, project_id, readOnly } = props;
391392
if (
392393
!(
393-
description.type == "disk" ||
394-
description.type == "vm" ||
395-
description.type == "quota"
394+
description.type === "disk" ||
395+
description.type === "vm" ||
396+
description.type === "quota"
396397
)
397398
) {
398399
throw Error("BUG -- incorrect typing");
@@ -403,7 +404,7 @@ function DescriptionColumnSiteLicense(props: DCProps) {
403404
return <pre>{JSON.stringify(description, undefined, 2)}</pre>;
404405
}
405406
const { input } = cost;
406-
if (input.type == "cash-voucher") {
407+
if (input.type === "cash-voucher") {
407408
throw Error("incorrect typing");
408409
}
409410

@@ -423,7 +424,7 @@ function DescriptionColumnSiteLicense(props: DCProps) {
423424
}
424425

425426
function editableQuota() {
426-
if (input.type == "cash-voucher") return null;
427+
if (input.type === "cash-voucher") return null;
427428
return (
428429
<div>
429430
<div>{describeQuotaFromInfo(input)}</div>
@@ -433,9 +434,14 @@ function DescriptionColumnSiteLicense(props: DCProps) {
433434
}
434435

435436
// this could rely an the "type" field, but we rather check the data directly
436-
function editPage(): "site-license" | "vouchers" {
437-
if (input.type == "cash-voucher") {
437+
function editPage(): "site-license" | "vouchers" | "course" {
438+
if (input.type === "cash-voucher") {
438439
return "vouchers";
440+
} else if (
441+
description.type === "quota" &&
442+
description.source === "course"
443+
) {
444+
return "course";
439445
}
440446
return "site-license";
441447
}
@@ -451,7 +457,7 @@ function DescriptionColumnSiteLicense(props: DCProps) {
451457
<div style={DESCRIPTION_STYLE}>
452458
<div style={{ marginBottom: "8px" }}>
453459
<b>
454-
{input.subscription == "no"
460+
{input.subscription === "no"
455461
? describePeriod({ quota: input })
456462
: capitalize(input.subscription) + " subscription"}
457463
</b>

src/packages/next/components/store/index.tsx

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import { Alert, Layout } from "antd";
66
import { useRouter } from "next/router";
77
import { useEffect, useState, type JSX } from "react";
8+
89
import * as purchasesApi from "@cocalc/frontend/purchases/api";
910
import { COLORS } from "@cocalc/util/theme";
1011
import Anonymous from "components/misc/anonymous";
@@ -16,28 +17,19 @@ import useProfile from "lib/hooks/profile";
1617
import useCustomize from "lib/use-customize";
1718
import Cart from "./cart";
1819
import Checkout from "./checkout";
19-
import Processing from "./processing";
2020
import Congrats from "./congrats";
2121
import Menu from "./menu";
2222
import Overview from "./overview";
23+
import Processing from "./processing";
2324
import SiteLicense from "./site-license";
2425
import { StoreInplaceSignInOrUp } from "./store-inplace-signup";
26+
import { StorePagesTypes } from "./types";
2527
import Vouchers from "./vouchers";
2628

2729
const { Content } = Layout;
2830

2931
interface Props {
30-
page: (
31-
| "site-license"
32-
| "boost"
33-
| "dedicated"
34-
| "cart"
35-
| "checkout"
36-
| "processing"
37-
| "congrats"
38-
| "vouchers"
39-
| undefined
40-
)[];
32+
page: (StorePagesTypes | undefined)[];
4133
}
4234

4335
export default function StoreLayout({ page }: Props) {
@@ -131,7 +123,9 @@ export default function StoreLayout({ page }: Props) {
131123

132124
switch (main) {
133125
case "site-license":
134-
return <SiteLicense noAccount={noAccount} />;
126+
return <SiteLicense noAccount={noAccount} source="site-license" />;
127+
case "course":
128+
return <SiteLicense noAccount={noAccount} source="course" />;
135129
case "cart":
136130
return requireAccount(Cart);
137131
case "checkout":

src/packages/next/components/store/menu.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,29 @@
33
* License: MS-RSL – see LICENSE.md for details
44
*/
55

6-
import React, { useContext } from "react";
7-
import { Button, Menu, MenuProps, Flex, Spin } from "antd";
6+
import type { MenuProps } from "antd";
7+
import { Button, Flex, Menu, Spin } from "antd";
88
import { useRouter } from "next/router";
9+
import React, { useContext } from "react";
10+
11+
import { Icon } from "@cocalc/frontend/components/icon";
912
import { currency, round2down } from "@cocalc/util/misc";
1013
import { COLORS } from "@cocalc/util/theme";
11-
import { Icon } from "@cocalc/frontend/components/icon";
12-
import { StoreBalanceContext } from "../../lib/balance";
14+
import { StoreBalanceContext } from "lib/balance";
1315

1416
type MenuItem = Required<MenuProps>["items"][number];
1517

1618
const styles: { [k: string]: React.CSSProperties } = {
1719
menuBookend: {
1820
height: "100%",
1921
whiteSpace: "nowrap",
20-
flexGrow: 1,
22+
flex: "0 1 auto",
2123
textAlign: "end",
2224
},
2325
menu: {
2426
width: "100%",
2527
height: "100%",
28+
flex: "1 1 auto",
2629
border: 0,
2730
},
2831
menuRoot: {
@@ -38,7 +41,7 @@ const styles: { [k: string]: React.CSSProperties } = {
3841
maxWidth: "100%",
3942
flexGrow: 1,
4043
},
41-
};
44+
} as const;
4245

4346
export interface ConfigMenuProps {
4447
main?: string;
@@ -64,6 +67,7 @@ export default function ConfigMenu({ main }: ConfigMenuProps) {
6467
key: "site-license",
6568
icon: <Icon name="key" />,
6669
},
70+
{ label: "Course", key: "course", icon: <Icon name="graduation-cap" /> },
6771
{
6872
label: "Vouchers",
6973
key: "vouchers",

src/packages/next/components/store/overview.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,17 @@ export default function Overview() {
4747
</Paragraph>
4848
) : undefined}
4949
<OverviewRow>
50-
<Product icon="key" title="Licenses" href="/store/site-license">
50+
<Product icon="key" title="License" href="/store/site-license">
5151
Buy a license to upgrade projects, get internet access, more CPU, disk
5252
and memory.
5353
</Product>
54-
<Product href={"/store/vouchers"} icon="gift" title="Vouchers">
55-
Purchase a <A href={"/vouchers"}>voucher code</A> to make <SiteName />{" "}
56-
credit easily available to somebody else.
54+
<Product icon="graduation-cap" title="Course" href="/store/course">
55+
Purchase a license for teaching a course.
5756
</Product>
57+
<Paragraph style={{ textAlign: "center", width: "100%" }}>
58+
<Icon name="gift" /> Purchase a <A href={"/vouchers"}>voucher code</A>{" "}
59+
to make <SiteName /> credit easily available to somebody else.
60+
</Paragraph>
5861
<Divider />
5962
<Product
6063
href={"/features/compute-server"}
@@ -74,7 +77,7 @@ export default function Overview() {
7477
<A href="/store/cart">shopping cart</A> or go straight to{" "}
7578
<A href="/store/checkout">checkout</A>.
7679
</Paragraph>
77-
<Paragraph>
80+
<Paragraph style={{ marginBottom: "4em" }}>
7881
You can also browse your{" "}
7982
<A href="/settings/purchases">purchase history</A>,{" "}
8083
<A href="/settings/licenses">licenses</A>, and{" "}

0 commit comments

Comments
 (0)