Skip to content

Commit

Permalink
frontend/trial-banner: scaffolding for countdown timer
Browse files Browse the repository at this point in the history
  • Loading branch information
haraldschilly committed Jul 9, 2024
1 parent df75554 commit 21644c4
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 49 deletions.
1 change: 1 addition & 0 deletions src/packages/frontend/customize.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export interface CustomizeState {
logo_square: string;
max_upgrades: TypedMap<Partial<Upgrades>>;
nonfree_countries?: List<string>;
limit_free_project_uptime: number; // minutes
onprem_quota_heading: string;
organization_email: string;
organization_name: string;
Expand Down
20 changes: 10 additions & 10 deletions src/packages/frontend/editors/stopwatch/stopwatch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ import {
PlayCircleTwoTone,
StopTwoTone,
} from "@ant-design/icons";
import { redux, useForceUpdate } from "@cocalc/frontend/app-framework";
import { Icon } from "@cocalc/frontend/components/icon";
import { Button, Col, Modal, Row, TimePicker, Tooltip } from "antd";
import type { Dayjs } from "dayjs";
import dayjs from "dayjs";
import { CSSProperties, useEffect, useState } from "react";

import { redux, useForceUpdate } from "@cocalc/frontend/app-framework";
import { Icon } from "@cocalc/frontend/components/icon";
import MarkdownInput from "@cocalc/frontend/editors/markdown-input/multimode";
import StaticMarkdown from "@cocalc/frontend/editors/slate/static-markdown";
import { useFrameContext } from "@cocalc/frontend/frame-editors/frame-tree/frame-context";
Expand All @@ -36,7 +36,7 @@ interface StopwatchProps {
state: TimerState; // 'paused' or 'running' or 'stopped'
time: number; // when entered this state
countdown?: number; // if given, this is a countdown timer, counting down from this many seconds.
clickButton: (str: string) => void;
clickButton?: (str: string) => void;
setLabel?: (str: string) => void;
setCountdown?: (time: number) => void; // time in seconds
compact?: boolean;
Expand Down Expand Up @@ -71,7 +71,7 @@ export default function Stopwatch(props: StopwatchProps) {
>
<Button
icon={<PlayCircleTwoTone />}
onClick={() => props.clickButton("start")}
onClick={() => props.clickButton?.("start")}
style={!props.compact ? { width: "8em" } : undefined}
>
{!props.compact ? "Start" : undefined}
Expand All @@ -98,7 +98,7 @@ export default function Stopwatch(props: StopwatchProps) {
>
<Button
icon={<StopTwoTone />}
onClick={() => props.clickButton("reset")}
onClick={() => props.clickButton?.("reset")}
>
{!props.compact ? "Reset" : undefined}
</Button>
Expand Down Expand Up @@ -126,7 +126,7 @@ export default function Stopwatch(props: StopwatchProps) {
time.second() + time.minute() * 60 + time.hour() * 60 * 60,
);
// timeout so the setcountdown can fully propagate through flux; needed for whiteboard
setTimeout(() => props.clickButton("reset"), 0);
setTimeout(() => props.clickButton?.("reset"), 0);
}
}}
showNow={false}
Expand All @@ -152,7 +152,7 @@ export default function Stopwatch(props: StopwatchProps) {
>
<Button
icon={<DeleteTwoTone />}
onClick={() => props.clickButton("delete")}
onClick={() => props.clickButton?.("delete")}
>
{!props.compact ? "Delete" : undefined}
</Button>
Expand All @@ -165,7 +165,7 @@ export default function Stopwatch(props: StopwatchProps) {
<Tooltip mouseEnterDelay={1} title="Pause the stopwatch">
<Button
icon={<PauseCircleTwoTone />}
onClick={() => props.clickButton("pause")}
onClick={() => props.clickButton?.("pause")}
style={!props.compact ? { width: "8em" } : undefined}
>
{!props.compact ? "Pause" : undefined}
Expand Down Expand Up @@ -229,13 +229,13 @@ export default function Stopwatch(props: StopwatchProps) {
}
open
onOk={() => {
props.clickButton("reset");
props.clickButton?.("reset");
redux
.getProjectActions(frame.project_id)
?.open_file({ path: frame.path });
}}
onCancel={() => {
props.clickButton("reset");
props.clickButton?.("reset");
}}
>
{props.label && <StaticMarkdown value={props.label} />}
Expand Down
192 changes: 157 additions & 35 deletions src/packages/frontend/project/trial-banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,24 @@
* License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details
*/

import { Alert, Tag } from "antd";
import { Alert, Modal, Tag } from "antd";
import humanizeList from "humanize-list";
import { join } from "path";

import {
CSS,
React,
useEffect,
useForceUpdate,
useMemo,
useState,
redux,
useTypedRedux,
} from "@cocalc/frontend/app-framework";
import { A, Icon, Paragraph } from "@cocalc/frontend/components";
import { A, Icon, Paragraph, Text } from "@cocalc/frontend/components";
import { SiteName } from "@cocalc/frontend/customize";
import { appBasePath } from "@cocalc/frontend/customize/app-base-path";
import { TimeAmount } from "@cocalc/frontend/editors/stopwatch/time";
import { open_new_tab } from "@cocalc/frontend/misc";
import {
SiteLicenseInput,
useManagedLicenses,
Expand All @@ -26,9 +31,10 @@ import {
EVALUATION_PERIOD_DAYS,
LICENSE_MIN_PRICE,
} from "@cocalc/util/consts/billing";
import { server_time } from "@cocalc/util/relative-time";
import { server_time } from "@cocalc/util/misc";
import { COLORS, DOC_URL } from "@cocalc/util/theme";
import { useAllowedFreeProjectToRun } from "./client-side-throttle";
import { useProjectContext } from "./context";
import { applyLicense } from "./settings/site-license";

export const DOC_TRIAL = "https://doc.cocalc.com/trial.html";
Expand Down Expand Up @@ -133,22 +139,22 @@ export const TrialBanner: React.FC<BannerProps> = React.memo(
<strong>No upgrades</strong>
);

function renderComputeServer() {
return (
<a
style={a_style}
onClick={() => {
const actions = redux.getProjectActions(project_id);
actions.setState({ create_compute_server: true });
actions.set_active_tab("servers", {
change_history: true,
});
}}
>
using a compute server
</a>
);
}
// function renderComputeServer() {
// return (
// <a
// style={a_style}
// onClick={() => {
// const actions = redux.getProjectActions(project_id);
// actions.setState({ create_compute_server: true });
// actions.set_active_tab("servers", {
// change_history: true,
// });
// }}
// >
// using a compute server
// </a>
// );
// }

function renderBuyAndUpgrade(text: string = "with a license"): JSX.Element {
return (
Expand All @@ -160,9 +166,10 @@ export const TrialBanner: React.FC<BannerProps> = React.memo(
asLink={true}
style={{ padding: 0, fontSize: style.fontSize, ...a_style }}
/>
. Price starts at {LICENSE_MIN_PRICE}.{" "}
.<br />
Price starts at {LICENSE_MIN_PRICE}.{" "}
<a style={a_style} onClick={() => setShowAddLicense(true)}>
Apply your license to this project.
Apply your license to this project
</a>
</>
);
Expand All @@ -183,7 +190,7 @@ export const TrialBanner: React.FC<BannerProps> = React.memo(
return (
<span>
{trial_project} You can improve hosting quality and get internet
access {renderComputeServer()} or {renderBuyAndUpgrade()}.
access {/* {renderComputeServer()} */} or {renderBuyAndUpgrade()}.
<br />
Otherwise, {humanizeList([...NO_HOST, NO_INTERNET])}
{"."}
Expand Down Expand Up @@ -251,20 +258,28 @@ export const TrialBanner: React.FC<BannerProps> = React.memo(
return null;
}

function renderClose() {
return (
<Tag
style={{ marginTop: "10px", fontSize: style.fontSize }}
color="#faad14"
>
<Icon name="times" /> Dismiss
</Tag>
);
}

function renderCountDown() {
if (closable) return;

return <CountdownProject fontSize={style.fontSize} />;
}

return (
<Alert
type="warning"
closable={closable}
closeIcon={
closable ? (
<Tag
style={{ marginTop: "10px", fontSize: style.fontSize }}
color="#faad14"
>
<Icon name="times" /> Dismiss
</Tag>
) : undefined
}
closeIcon={renderClose()}
style={style}
banner={true}
showIcon={!closable || (internet && host)}
Expand All @@ -286,15 +301,16 @@ export const TrialBanner: React.FC<BannerProps> = React.memo(
padding: 0,
}}
>
{renderCountDown()}
{renderMessage()} {renderLearnMore(style.color)}
</Paragraph>
{showAddLicense && (
{showAddLicense ? (
<BannerApplySiteLicense
project_id={project_id}
projectSiteLicenses={projectSiteLicenses}
setShowAddLicense={setShowAddLicense}
/>
)}
) : undefined}
</>
}
/>
Expand Down Expand Up @@ -356,3 +372,109 @@ export const BannerApplySiteLicense: React.FC<ApplyLicenseProps> = (
</>
);
};

interface CountdownProjectProps {
fontSize: CSS["fontSize"];
}

function CountdownProject({ fontSize }: CountdownProjectProps) {
const { status, project, project_id } = useProjectContext();
const limit_min = useTypedRedux("customize", "limit_free_project_uptime");
const [showInfo, setShowInfo] = useState<boolean>(false);
const update = useForceUpdate();

useEffect(() => {
const interval = setInterval(update, 1000);
return () => clearInterval(interval);
}, []);

if (
status.get("state") !== "running" ||
project == null ||
limit_min == null ||
limit_min <= 0
) {
return null;
}

// start_ts is e.g. 1508576664416
const start_ts = project.getIn(["status", "start_ts"]);
if (start_ts == undefined) return null;

const shutdown_ts = start_ts + 1000 * 60 * 60 * limit_min;
const countdown = shutdown_ts - server_time().getTime();

if (countdown < 0) return null;

function renderInfo() {
return (
<Modal
title={"Project Shutdown"}
open={showInfo}
onOk={() => open_new_tab(BUY_A_LICENSE_URL, true)}
onCancel={() => setShowInfo(false)}
>
<Paragraph strong>
This is a call to support the <SiteName /> project by{" "}
<BuyLicenseForProject
project_id={project_id}
buyText={"purchasing a license"}
voucherText={"redeeming a voucher"}
asLink={true}
/>
.
</Paragraph>
<Paragraph>
Behind this site are <A href={"/about/team"}>humans working hard</A>{" "}
to keep the service running and improving it constantly. Running your
computations <A href={"/info/status"}>in our cluster</A> and storing
your files costs us money as well.
</Paragraph>
<Paragraph>
<SiteName /> receives no funding from large venture captital
organizations or charitable foundations. The site depends entirely{" "}
<Text strong>on your financial support</Text> to continue operating.
Without your financial support this service will not survive
long-term!
</Paragraph>
<Paragraph>
<A href={"https://doc.cocalc.com/trial.html"}>Trial projects</A> have
a maximum uptime of {limit_min} minutes. After that period, the
project will stop and interrupt your work. The state in your
interactive files like Jupyter Notebooks will be lost. The files
themselves will be saved, though.
</Paragraph>
<Paragraph strong>
This shutdown timer only exists for projects without upgrades!
</Paragraph>
</Modal>
);
}

return (
<>
{renderInfo()}
<Tag
style={{
marginTop: "5px",
fontSize,
float: "right",
fontWeight: "bold",
color: COLORS.ANTD_RED,
cursor: "pointer",
}}
color={COLORS.GRAY_LL}
onClick={() => setShowInfo(true)}
>
<TimeAmount
key={"time"}
amount={countdown}
compact={true}
showIcon={true}
countdown={countdown}
style={{ color: COLORS.ANTD_RED }}
/>
</Tag>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ export const BuyLicenseForProject: React.FC<Props> = ({

function renderBuyButton() {
if (asLink) {
return <A href={url("store/site-license")}>{buyText}...</A>;
return (
<A href={url("store/site-license")} style={style}>
{buyText}
</A>
);
}
return (
<Button
Expand All @@ -67,7 +71,11 @@ export const BuyLicenseForProject: React.FC<Props> = ({

function renderVoucherButton() {
if (asLink) {
return <A href={url("redeem")}>{voucherText}...</A>;
return (
<A href={url("redeem")} style={style}>
{voucherText}
</A>
);
}
return (
<Button
Expand Down
4 changes: 2 additions & 2 deletions src/packages/util/consts/billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export const AVG_YEAR_DAYS = 12 * AVG_MONTH_DAYS;
export const ONE_MONTH_MS = AVG_MONTH_DAYS * ONE_DAY_MS;

// throughout the UI, we show this price as the minimum (per month)
export const LICENSE_MIN_PRICE = "about $4/month";
export const LICENSE_MIN_PRICE = "about $5/month";

// Trial Banner in the UI
export const EVALUATION_PERIOD_DAYS = 10;
export const BANNER_NON_DISMISSABLE_DAYS = 30;
export const BANNER_NON_DISMISSABLE_DAYS = 15;
Loading

0 comments on commit 21644c4

Please sign in to comment.