flotilla/src/app/components/hosting/PricingTable.svelte

130 lines
4.2 KiB
Svelte

<script lang="ts">
import cx from "classnames"
import Check from "@assets/icons/check.svg?dataurl"
import Close from "@assets/icons/close.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Link from "@lib/components/Link.svelte"
import type {Plan} from "@app/hosting"
type Props = {
plans: Plan[]
selectable?: boolean
value?: string
onSelect?: (planId: string) => void
onCta?: (planId: string) => void
}
const {plans, selectable = false, value, onSelect, onCta}: Props = $props()
const priceLabel = (amount: number) => (amount === 0 ? "Free" : `$${amount / 100}`)
const memberLabel = (members: Plan["members"]) =>
members ? `Up to ${members} members` : "Unlimited members"
const select = (planId: string) => () => onSelect?.(planId)
const start = (planId: string) => () => onCta?.(planId)
const visiblePlans = $derived(plans.filter(plan => !plan.hidden))
const gridCols: Record<number, string> = {
1: "",
2: "@md:grid-cols-2",
3: "@md:grid-cols-2 @3xl:grid-cols-3",
4: "@md:grid-cols-2 @3xl:grid-cols-4",
}
const cardCount = $derived(visiblePlans.length + (selectable ? 0 : 1))
</script>
{#snippet feature(present: boolean, label: string)}
<li class={cx("flex items-start gap-2", {"text-content-subtle": !present})}>
<Icon
icon={present ? Check : Close}
size={4}
class={cx("mt-0.5 shrink-0", {"text-primary": present})} />
{label}
</li>
{/snippet}
{#snippet cardBody(plan: Plan, isPopular: boolean)}
{#if isPopular && !selectable}
<span
class="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-primary px-3 py-1 text-xs font-bold tracking-wide text-primary-content">
POPULAR
</span>
{/if}
<h3 class="mb-1 text-lg font-bold">{plan.name}</h3>
<div class="mb-8">
<span class="text-4xl font-extrabold">{priceLabel(plan.amount)}</span>
<span class="ml-1 text-sm text-content-muted">/ mo</span>
</div>
<ul class="mb-8 flex flex-col gap-3 text-sm">
<li class="flex items-start gap-2">
<Icon icon={Check} size={4} class="mt-0.5 shrink-0 text-primary" />
{memberLabel(plan.members)}
</li>
{@render feature(plan.blossom, "Media storage")}
{@render feature(plan.livekit, "Video calls")}
</ul>
{#if !selectable}
<Button
class={cx("button button-block mt-auto", {
"button-primary": isPopular,
"button-neutral": !isPopular,
})}
onclick={start(plan.id)}>
Get started
</Button>
{/if}
{/snippet}
<div class="@container">
<div class={cx("grid grid-cols-1 items-start gap-6", gridCols[cardCount] ?? "@3xl:grid-cols-4")}>
{#each visiblePlans as plan (plan.id)}
{@const isPopular = plan.id === "basic"}
{#if selectable}
<Button
class={cx("card card-interactive relative flex flex-col text-left", {
"border-primary": value === plan.id,
})}
onclick={select(plan.id)}>
{@render cardBody(plan, isPopular)}
</Button>
{:else}
<div class={cx("card relative flex flex-col", {"border-primary": isPopular})}>
{@render cardBody(plan, isPopular)}
</div>
{/if}
{/each}
{#if !selectable}
<div class="card relative flex flex-col">
<h3 class="mb-1 text-lg font-bold">Custom</h3>
<div class="mb-8">
<span class="text-4xl font-extrabold">Let's talk</span>
</div>
<ul class="mb-8 flex flex-col gap-3 text-sm">
<li class="flex items-start gap-2">
<Icon icon={Check} size={4} class="mt-0.5 shrink-0 text-primary" />
White-labeled app
</li>
<li class="flex items-start gap-2">
<Icon icon={Check} size={4} class="mt-0.5 shrink-0 text-primary" />
Dedicated support
</li>
<li class="flex items-start gap-2">
<Icon icon={Check} size={4} class="mt-0.5 shrink-0 text-primary" />
Custom feature development
</li>
</ul>
<Link
external
href="https://cal.com/coracle.social/30min"
class="button button-neutral button-block mt-auto text-center">
Contact us
</Link>
</div>
{/if}
</div>
</div>