Add commercial use upsell banners

This commit is contained in:
Gregory Schier
2026-06-19 16:09:43 -07:00
parent 3de9a1edd4
commit 4092511f22
11 changed files with 184 additions and 40 deletions
@@ -1,57 +1,84 @@
import type { Color } from "@yaakapp-internal/plugins";
import type { BannerProps } from "@yaakapp-internal/ui";
import { Banner, HStack } from "@yaakapp-internal/ui";
import { Banner } from "@yaakapp-internal/ui";
import classNames from "classnames";
import { useKeyValue } from "../../hooks/useKeyValue";
import type { ButtonProps } from "./Button";
import { Button } from "./Button";
export function DismissibleBanner({
children,
className,
dismissForDays,
id,
actions,
...props
}: BannerProps & {
id: string;
actions?: { label: string; onClick: () => void; color?: Color }[];
dismissForDays?: number;
actions?: {
label: string;
onClick: () => void;
color?: Color;
variant?: ButtonProps["variant"];
}[];
}) {
const { set: setDismissed, value: dismissed } = useKeyValue<boolean>({
const {
isLoading,
set: setDismissed,
value: dismissed,
} = useKeyValue<boolean | string>({
namespace: "global",
key: ["dismiss-banner", id],
fallback: false,
});
if (dismissed) return null;
if (isLoading || isDismissed(dismissed, dismissForDays)) return null;
return (
<Banner
className={classNames(className, "relative grid grid-cols-[1fr_auto] gap-3")}
{...props}
>
{children}
<HStack space={1.5}>
{actions?.map((a) => (
<Button
key={a.label}
variant="border"
color={a.color ?? props.color}
size="xs"
onClick={a.onClick}
title={a.label}
>
{a.label}
</Button>
))}
<Button
variant="border"
color={props.color}
size="xs"
onClick={() => setDismissed((d) => !d)}
title="Dismiss message"
>
Dismiss
</Button>
</HStack>
<Banner className={classNames(className, "relative")} {...props}>
<div className="@container">
<div className="grid gap-2 @[34rem]:grid-cols-[minmax(0,1fr)_auto] @[34rem]:items-center @[34rem]:gap-3">
{children}
<div className="flex flex-wrap gap-1.5 @[34rem]:justify-end">
<Button
variant="border"
color={props.color}
size="xs"
onClick={() => setDismissed(dismissForDays == null ? true : new Date().toISOString())}
title="Dismiss message"
>
Dismiss
</Button>
{actions?.map((a) => (
<Button
key={a.label}
variant={a.variant ?? "border"}
color={a.color ?? props.color}
size="xs"
onClick={a.onClick}
title={a.label}
>
{a.label}
</Button>
))}
</div>
</div>
</div>
</Banner>
);
}
function isDismissed(
dismissed: boolean | string | null,
dismissForDays: number | undefined,
): boolean {
if (dismissed === false || dismissed == null) return false;
if (dismissed === true) return true;
if (dismissForDays == null) return dismissed.length > 0;
const dismissedAt = new Date(dismissed).getTime();
if (Number.isNaN(dismissedAt)) return false;
return Date.now() - dismissedAt < dismissForDays * 24 * 60 * 60 * 1000;
}