Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/next/components/store/other-items.tsx
Views: 687
/*1The "Saved for Later" section below the shopping cart.2*/34import { useEffect, useMemo, useState } from "react";5import useAPI from "lib/hooks/api";6import apiPost from "lib/api/post";7import useIsMounted from "lib/hooks/mounted";8import {9Alert,10Button,11Input,12Menu,13MenuProps,14Row,15Col,16Popconfirm,17Table,18} from "antd";19import { DisplayCost, describeItem } from "./site-license-cost";20import { computeCost } from "@cocalc/util/licenses/store/compute-cost";21import Loading from "components/share/loading";22import { Icon } from "@cocalc/frontend/components/icon";23import { search_split, search_match } from "@cocalc/util/misc";24import { ProductColumn } from "./cart";2526type MenuItem = Required<MenuProps>["items"][number];27type Tab = "saved-for-later" | "buy-it-again";2829interface Props {30onChange: () => void;31cart: { result: any }; // returned by useAPI; used to track when it updates.32}3334export default function OtherItems({ onChange, cart }) {35const [tab, setTab] = useState<Tab>("saved-for-later");36const [search, setSearch] = useState<string>("");3738const items: MenuItem[] = [39{ label: "Saved For Later", key: "saved-for-later" as Tab },40{ label: "Buy It Again", key: "buy-it-again" as Tab },41];4243return (44<div>45<Row>46<Col sm={18} xs={24}>47<Menu48selectedKeys={[tab]}49mode="horizontal"50onSelect={(e) => {51setTab(e.keyPath[0] as Tab);52}}53items={items}54/>55</Col>56<Col sm={6}>57<div58style={{59height: "100%",60borderBottom: "1px solid #eee" /* hack to match menu */,61display: "flex",62flexDirection: "column",63alignContent: "center",64justifyContent: "center",65paddingRight: "5px",66}}67>68<Input.Search69style={{ width: "100%" }}70placeholder="Search..."71value={search}72onChange={(e) => setSearch(e.target.value)}73/>74</div>75</Col>76</Row>77<Items78onChange={onChange}79cart={cart}80tab={tab}81search={search.toLowerCase()}82/>83</div>84);85}8687interface ItemsProps extends Props {88tab: Tab;89search: string;90}9192function Items({ onChange, cart, tab, search }: ItemsProps) {93const isMounted = useIsMounted();94const [updating, setUpdating] = useState<boolean>(false);95const get = useAPI(96"/shopping/cart/get",97tab == "buy-it-again" ? { purchased: true } : { removed: true },98);99const items = useMemo(() => {100if (!get.result) return undefined;101const x: any[] = [];102const v = search_split(search);103for (const item of get.result) {104if (search && !search_match(JSON.stringify(item).toLowerCase(), v))105continue;106item.cost = computeCost(item.description);107x.push(item);108}109return x;110}, [get.result, search]);111112useEffect(() => {113get.call();114}, [cart.result]);115116if (get.error) {117return <Alert type="error" message={get.error} />;118}119if (get.result == null || items == null) {120return <Loading center />;121}122123async function reload() {124if (!isMounted.current) return;125setUpdating(true);126try {127await get.call();128} finally {129if (isMounted.current) {130setUpdating(false);131}132}133}134135if (items.length == 0) {136return (137<div style={{ padding: "15px", textAlign: "center", fontSize: "10pt" }}>138{tab == "buy-it-again"139? `No ${search ? "matching" : ""} previously purchased items.`140: `No ${search ? "matching" : ""} items saved for later.`}141</div>142);143}144145const columns = [146{147responsive: ["xs" as "xs"],148render: ({ id, cost, description }) => {149return (150<div>151<DescriptionColumn152{...{153id,154cost,155description,156updating,157setUpdating,158isMounted,159reload,160onChange,161tab,162}}163/>164<div>165<b style={{ fontSize: "11pt" }}>166<DisplayCost cost={cost} simple oneLine />167</b>168</div>169</div>170);171},172},173{174responsive: ["sm" as "sm"],175title: "Product",176align: "center" as "center",177render: (_, { product }) => <ProductColumn product={product} />,178},179{180responsive: ["sm" as "sm"],181width: "60%",182render: (_, { id, cost, description }) => (183<DescriptionColumn184{...{185id,186cost,187description,188updating,189setUpdating,190isMounted,191onChange,192reload,193tab,194}}195/>196),197},198{199responsive: ["sm" as "sm"],200title: "Price",201align: "right" as "right",202render: (_, { cost }) => (203<b style={{ fontSize: "11pt" }}>204<DisplayCost cost={cost} simple />205</b>206),207},208];209210return (211<Table212showHeader={false}213columns={columns}214dataSource={items}215rowKey={"id"}216pagination={{ hideOnSinglePage: true }}217/>218);219}220221function DescriptionColumn({222id,223cost,224description,225updating,226setUpdating,227isMounted,228onChange,229reload,230tab,231}) {232const { input } = cost;233return (234<>235<div style={{ fontSize: "12pt" }}>236{description.title && (237<div>238<b>{description.title}</b>239</div>240)}241{description.description && <div>{description.description}</div>}242{describeItem({ info: input })}243</div>244<div style={{ marginTop: "5px" }}>245<Button246disabled={updating}247onClick={async () => {248setUpdating(true);249try {250await apiPost("/shopping/cart/add", {251id,252purchased: tab == "buy-it-again",253});254if (!isMounted.current) return;255onChange();256await reload();257} finally {258if (!isMounted.current) return;259setUpdating(false);260}261}}262>263<Icon name="shopping-cart" />{" "}264{tab == "buy-it-again" ? "Add to Cart" : "Move to Cart"}265</Button>266{tab == "saved-for-later" && (267<Popconfirm268title={"Are you sure you want to delete this item?"}269onConfirm={async () => {270setUpdating(true);271try {272await apiPost("/shopping/cart/delete", { id });273if (!isMounted.current) return;274await reload();275} finally {276if (!isMounted.current) return;277setUpdating(false);278}279}}280okText={"Yes, delete this item"}281cancelText={"Cancel"}282>283<Button284disabled={updating}285type="dashed"286style={{ margin: "0 5px" }}287>288<Icon name="trash" /> Delete289</Button>290</Popconfirm>291)}292</div>293</>294);295}296297298