Path: blob/master/src/packages/next/components/store/other-items.tsx
1450 views
/*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>("buy-it-again");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.Search69allowClear70style={{ width: "100%" }}71placeholder="Search..."72value={search}73onChange={(e) => setSearch(e.target.value)}74/>75</div>76</Col>77</Row>78<Items79onChange={onChange}80cart={cart}81tab={tab}82search={search.toLowerCase()}83/>84</div>85);86}8788interface ItemsProps extends Props {89tab: Tab;90search: string;91}9293function Items({ onChange, cart, tab, search }: ItemsProps) {94const isMounted = useIsMounted();95const [updating, setUpdating] = useState<boolean>(false);96const get = useAPI(97"/shopping/cart/get",98tab == "buy-it-again" ? { purchased: true } : { removed: true },99);100const items = useMemo(() => {101if (!get.result) {102return undefined;103}104const x: any[] = [];105const v = search_split(search);106for (const item of get.result) {107if (search && !search_match(JSON.stringify(item).toLowerCase(), v)) {108continue;109}110try {111item.cost = computeCost(item.description);112} catch (_err) {113// deprecated, so do not include114continue;115}116x.push(item);117}118return x;119}, [get.result, search]);120121useEffect(() => {122get.call();123}, [cart.result]);124125if (get.error) {126return <Alert type="error" message={get.error} />;127}128if (get.result == null || items == null) {129return <Loading large center />;130}131132async function reload() {133if (!isMounted.current) return;134setUpdating(true);135try {136await get.call();137} finally {138if (isMounted.current) {139setUpdating(false);140}141}142}143144if (items.length == 0) {145return (146<div style={{ padding: "15px", textAlign: "center", fontSize: "10pt" }}>147{tab == "buy-it-again"148? `No ${search ? "matching" : ""} previously purchased items.`149: `No ${search ? "matching" : ""} items saved for later.`}150</div>151);152}153154const columns = [155{156responsive: ["xs" as "xs"],157render: ({ id, cost, description }) => {158return (159<div>160<DescriptionColumn161{...{162id,163cost,164description,165updating,166setUpdating,167isMounted,168reload,169onChange,170tab,171}}172/>173<div>174<b style={{ fontSize: "11pt" }}>175<DisplayCost cost={cost} simple oneLine />176</b>177</div>178</div>179);180},181},182{183responsive: ["sm" as "sm"],184title: "Product",185align: "center" as "center",186render: (_, { product }) => <ProductColumn product={product} />,187},188{189responsive: ["sm" as "sm"],190width: "60%",191render: (_, { id, cost, description }) => (192<DescriptionColumn193{...{194id,195cost,196description,197updating,198setUpdating,199isMounted,200onChange,201reload,202tab,203}}204/>205),206},207{208responsive: ["sm" as "sm"],209title: "Price",210align: "right" as "right",211render: (_, { cost }) => (212<b style={{ fontSize: "11pt" }}>213<DisplayCost cost={cost} simple />214</b>215),216},217];218219return (220<Table221showHeader={false}222columns={columns}223dataSource={items}224rowKey={"id"}225pagination={{ hideOnSinglePage: true }}226/>227);228}229230function DescriptionColumn({231id,232cost,233description,234updating,235setUpdating,236isMounted,237onChange,238reload,239tab,240}) {241const { input } = cost ?? {};242return (243<>244<div style={{ fontSize: "12pt" }}>245{description.title && (246<div>247<b>{description.title}</b>248</div>249)}250{description.description && <div>{description.description}</div>}251{input != null && describeItem({ info: input })}252</div>253<div style={{ marginTop: "5px" }}>254<Button255disabled={updating}256onClick={async () => {257setUpdating(true);258try {259await apiPost("/shopping/cart/add", {260id,261purchased: tab == "buy-it-again",262});263if (!isMounted.current) return;264onChange();265await reload();266} finally {267if (!isMounted.current) return;268setUpdating(false);269}270}}271>272<Icon name="shopping-cart" />{" "}273{tab == "buy-it-again" ? "Add to Cart" : "Move to Cart"}274</Button>275{tab == "saved-for-later" && (276<Popconfirm277title={"Are you sure you want to delete this item?"}278onConfirm={async () => {279setUpdating(true);280try {281await apiPost("/shopping/cart/delete", { id });282if (!isMounted.current) return;283await reload();284} finally {285if (!isMounted.current) return;286setUpdating(false);287}288}}289okText={"Yes, delete this item"}290cancelText={"Cancel"}291>292<Button293disabled={updating}294type="dashed"295style={{ margin: "0 5px" }}296>297<Icon name="trash" /> Delete298</Button>299</Popconfirm>300)}301</div>302</>303);304}305306307