Skip to content

next/store: "course" purchases #8437

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions src/packages/next/components/store/add-box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { addToCart } from "./add-to-cart";
import { DisplayCost } from "./site-license-cost";
import { periodicCost } from "@cocalc/util/licenses/purchase/compute-cost";
import { decimalDivide } from "@cocalc/util/stripe/calc";
import { LicenseType } from "./types";

export const ADD_STYLE = {
display: "inline-block",
Expand All @@ -37,6 +38,7 @@ interface Props {
dedicatedItem?: boolean;
disabled?: boolean;
noAccount: boolean;
type: LicenseType;
}

export function AddBox({
Expand All @@ -48,6 +50,7 @@ export function AddBox({
dedicatedItem = false,
noAccount,
disabled = false,
type,
}: Props) {
if (cost?.input.type == "cash-voucher") {
return null;
Expand Down Expand Up @@ -76,7 +79,8 @@ export function AddBox({
}}
message={
<>
{money(round2up(costPer))} <b>per project</b>{" "}
{money(round2up(costPer))}{" "}
<b>per {type === "course" ? "student" : "project"}</b>{" "}
{!!cost.period && cost.period != "range" ? cost.period : ""}
</>
}
Expand Down Expand Up @@ -175,8 +179,8 @@ export function AddToCartButton({
{clicked
? "Moving to Cart..."
: router.query.id != null
? "Save Changes"
: "Add to Cart"}
? "Save Changes"
: "Add to Cart"}
{clicked && <Spin style={{ marginLeft: "15px" }} />}
</Button>
);
Expand Down
20 changes: 7 additions & 13 deletions src/packages/next/components/store/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { Alert, Layout } from "antd";
import { useRouter } from "next/router";
import { useEffect, useState, type JSX } from "react";

import * as purchasesApi from "@cocalc/frontend/purchases/api";
import { COLORS } from "@cocalc/util/theme";
import Anonymous from "components/misc/anonymous";
Expand All @@ -16,28 +17,19 @@ import useProfile from "lib/hooks/profile";
import useCustomize from "lib/use-customize";
import Cart from "./cart";
import Checkout from "./checkout";
import Processing from "./processing";
import Congrats from "./congrats";
import Menu from "./menu";
import Overview from "./overview";
import Processing from "./processing";
import SiteLicense from "./site-license";
import { StoreInplaceSignInOrUp } from "./store-inplace-signup";
import { StorePagesTypes } from "./types";
import Vouchers from "./vouchers";

const { Content } = Layout;

interface Props {
page: (
| "site-license"
| "boost"
| "dedicated"
| "cart"
| "checkout"
| "processing"
| "congrats"
| "vouchers"
| undefined
)[];
page: (StorePagesTypes | undefined)[];
}

export default function StoreLayout({ page }: Props) {
Expand Down Expand Up @@ -131,7 +123,9 @@ export default function StoreLayout({ page }: Props) {

switch (main) {
case "site-license":
return <SiteLicense noAccount={noAccount} />;
return <SiteLicense noAccount={noAccount} type="license" />;
case "course":
return <SiteLicense noAccount={noAccount} type="course" />;
case "cart":
return requireAccount(Cart);
case "checkout":
Expand Down
16 changes: 10 additions & 6 deletions src/packages/next/components/store/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,29 @@
* License: MS-RSL – see LICENSE.md for details
*/

import React, { useContext } from "react";
import { Button, Menu, MenuProps, Flex, Spin } from "antd";
import type { MenuProps } from "antd";
import { Button, Flex, Menu, Spin } from "antd";
import { useRouter } from "next/router";
import React, { useContext } from "react";

import { Icon } from "@cocalc/frontend/components/icon";
import { currency, round2down } from "@cocalc/util/misc";
import { COLORS } from "@cocalc/util/theme";
import { Icon } from "@cocalc/frontend/components/icon";
import { StoreBalanceContext } from "../../lib/balance";
import { StoreBalanceContext } from "lib/balance";

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

const styles: { [k: string]: React.CSSProperties } = {
menuBookend: {
height: "100%",
whiteSpace: "nowrap",
flexGrow: 1,
flex: "0 1 auto",
textAlign: "end",
},
menu: {
width: "100%",
height: "100%",
flex: "1 1 auto",
border: 0,
},
menuRoot: {
Expand All @@ -38,7 +41,7 @@ const styles: { [k: string]: React.CSSProperties } = {
maxWidth: "100%",
flexGrow: 1,
},
};
} as const;

export interface ConfigMenuProps {
main?: string;
Expand All @@ -64,6 +67,7 @@ export default function ConfigMenu({ main }: ConfigMenuProps) {
key: "site-license",
icon: <Icon name="key" />,
},
{ label: "Course", key: "course", icon: <Icon name="graduation-cap" /> },
{
label: "Vouchers",
key: "vouchers",
Expand Down
13 changes: 8 additions & 5 deletions src/packages/next/components/store/overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,17 @@ export default function Overview() {
</Paragraph>
) : undefined}
<OverviewRow>
<Product icon="key" title="Licenses" href="/store/site-license">
<Product icon="key" title="License" href="/store/site-license">
Buy a license to upgrade projects, get internet access, more CPU, disk
and memory.
</Product>
<Product href={"/store/vouchers"} icon="gift" title="Vouchers">
Purchase a <A href={"/vouchers"}>voucher code</A> to make <SiteName />{" "}
credit easily available to somebody else.
<Product icon="graduation-cap" title="Course" href="/store/course">
Purchase a license for teaching a course.
</Product>
<Paragraph style={{ textAlign: "center", width: "100%" }}>
<Icon name="gift" /> Purchase a <A href={"/vouchers"}>voucher code</A>{" "}
to make <SiteName /> credit easily available to somebody else.
</Paragraph>
<Divider />
<Product
href={"/features/compute-server"}
Expand All @@ -74,7 +77,7 @@ export default function Overview() {
<A href="/store/cart">shopping cart</A> or go straight to{" "}
<A href="/store/checkout">checkout</A>.
</Paragraph>
<Paragraph>
<Paragraph style={{ marginBottom: "4em" }}>
You can also browse your{" "}
<A href="/settings/purchases">purchase history</A>,{" "}
<A href="/settings/licenses">licenses</A>, and{" "}
Expand Down
162 changes: 112 additions & 50 deletions src/packages/next/components/store/run-limit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,78 +4,136 @@
*/

import { Divider, Form } from "antd";

import A from "components/misc/A";
import IntegerSlider from "components/misc/integer-slider";
import { LicenseType } from "./types";
import { unreachable } from "@cocalc/util/misc";

export const MAX_ALLOWED_RUN_LIMIT = 10000;

interface RunLimitProps {
showExplanations: boolean;
form: any;
onChange: () => void;
disabled?: boolean;
boost?: boolean;
type: LicenseType;
}

export function RunLimit({
showExplanations,
form,
onChange,
disabled = false,
boost = false,
}) {
type,
}: RunLimitProps) {
function extra() {
if (!showExplanations) return;

return (
<div style={{ marginTop: "5px" }}>
{boost ? (
<div style={{ fontWeight: "bold" }}>
It's not necessary to match the run limit of the license you want to
boost!
switch (type) {
case "license":
return (
<div style={{ marginTop: "5px" }}>
{boost ? (
<div style={{ fontWeight: "bold" }}>
It's not necessary to match the run limit of the license you
want to boost!
</div>
) : undefined}
Simultaneously run this many projects using this license. You, and
anyone you share the license code with, can apply the license to an
unlimited number of projects, but it will only be used up to the run
limit. When{" "}
<A href="https://doc.cocalc.com/teaching-instructors.html">
teaching a course
</A>
,{" "}
<b>
<i>
the run limit is typically 2 more than the number of students
(one for each student, one for the shared project and one for
the instructor project)
</i>
</b>
.
</div>
) : undefined}
Simultaneously run this many projects using this license. You, and
anyone you share the license code with, can apply the license to an
unlimited number of projects, but it will only be used up to the run
limit. When{" "}
<A href="https://doc.cocalc.com/teaching-instructors.html">
teaching a course
</A>
,{" "}
<b>
<i>
the run limit is typically 2 more than the number of students (one
for each student, one for the shared project and one for the
);
case "course":
return (
<div style={{ marginTop: "5px" }}>
It's advised to select two more seatch than the number of students
(one for each student, one for the shared project and one for the
instructor project)
</i>
</b>
.
</div>
);
</div>
);

default:
unreachable(type);
}
}

return (
<>
<Divider plain>Simultaneous Project Upgrades</Divider>
<Form.Item
label="Run Limit"
name="run_limit"
initialValue={1}
extra={extra()}
>
<EditRunLimit
disabled={disabled}
onChange={(run_limit) => {
form.setFieldsValue({ run_limit });
onChange();
}}
/>
</Form.Item>
</>
);
switch (type) {
case "license":
return (
<>
<Divider plain>Simultaneous Project Upgrades</Divider>
<Form.Item
label="Run Limit"
name="run_limit"
initialValue={1}
extra={extra()}
>
<EditRunLimit
type={type}
disabled={disabled}
onChange={(run_limit) => {
form.setFieldsValue({ run_limit });
onChange();
}}
/>
</Form.Item>
</>
);

case "course":
return (
<>
<Divider plain>Size of Course</Divider>
<Form.Item
label="Students"
name="run_limit"
initialValue={25}
extra={extra()}
>
<EditRunLimit
type={type}
disabled={disabled}
onChange={(run_limit) => {
form.setFieldsValue({ run_limit });
onChange();
}}
/>
</Form.Item>
</>
);

default:
unreachable(type);
}
}

export function EditRunLimit({
function EditRunLimit({
value,
onChange,
disabled,
type,
}: {
value?;
onChange?;
disabled?;
value?: number;
onChange: (run_limit: number) => void;
disabled?: boolean;
type: LicenseType;
}) {
return (
<IntegerSlider
Expand All @@ -85,8 +143,12 @@ export function EditRunLimit({
max={300}
maxText={MAX_ALLOWED_RUN_LIMIT}
onChange={onChange}
units={"projects"}
presets={[1, 2, 10, 50, 100, 250, 500]}
units={type === "course" ? "students" : "projects"}
presets={
type === "course"
? [10, 25, 50, 75, 100, 125, 150, 200]
: [1, 2, 10, 50, 100, 250, 500]
}
/>
);
}
Loading