mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge pull request #1067 from appwrite/redesign-plan-summary-card
redesign: plan summary card
This commit is contained in:
Generated
+9
-9
@@ -7,8 +7,8 @@
|
||||
"name": "@appwrite/console",
|
||||
"dependencies": {
|
||||
"@appwrite.io/console": "^0.6.2",
|
||||
"@appwrite.io/pink": "^0.19.0",
|
||||
"@appwrite.io/pink-icons": "^0.19.0",
|
||||
"@appwrite.io/pink": "0.20.0",
|
||||
"@appwrite.io/pink-icons": "0.20.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@sentry/svelte": "^7.66.0",
|
||||
"@sentry/tracing": "^7.66.0",
|
||||
@@ -162,19 +162,19 @@
|
||||
"integrity": "sha512-3qYknuFwhvTN2GnPB8G1w5DgxfVorGtfj4uuEaXbTmMUmjA8fHaW5VkJriuUxCi6/TpxDxqV/hhEkdoXL+5H4w=="
|
||||
},
|
||||
"node_modules/@appwrite.io/pink": {
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@appwrite.io/pink/-/pink-0.19.0.tgz",
|
||||
"integrity": "sha512-b5yJCtkqRfikgqpi6rxN0je7FkLdvtSPQ3XNW49yLN7euHHuMr74qj8XvWrvv41ilikanvvUWUD2C8PLvmOy8Q==",
|
||||
"version": "0.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@appwrite.io/pink/-/pink-0.20.0.tgz",
|
||||
"integrity": "sha512-17anov7EL+Ic/4fNtCYgG3wA+hoeBXn/j+ZjkfZFfX3g66M8XzVTaQfsAH+EMZ3YjvWuKlHtn+bY4BkGwvMIzQ==",
|
||||
"dependencies": {
|
||||
"@appwrite.io/pink-icons": "0.19.0",
|
||||
"@appwrite.io/pink-icons": "0.20.0",
|
||||
"normalize.css": "^8.0.1",
|
||||
"the-new-css-reset": "^1.11.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@appwrite.io/pink-icons": {
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@appwrite.io/pink-icons/-/pink-icons-0.19.0.tgz",
|
||||
"integrity": "sha512-qXzIQrY9OVFUJ7/qCf5oRJqJEJ+ECQpzlU0JiLycbPnu/nbq+pbaFDPma+7cTHsaD8znRC8X+a7P22JNrsN0rg=="
|
||||
"version": "0.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@appwrite.io/pink-icons/-/pink-icons-0.20.0.tgz",
|
||||
"integrity": "sha512-nfDy7qE6FQWVO1eC+3nwGjXkj/Zm2V50rhvyj0/HKq8RyWNWFGX31BZoMSim01vl/N5irBilpkaER6mLPp7oEw=="
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.24.2",
|
||||
|
||||
+2
-2
@@ -20,8 +20,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@appwrite.io/console": "^0.6.2",
|
||||
"@appwrite.io/pink": "^0.19.0",
|
||||
"@appwrite.io/pink-icons": "^0.19.0",
|
||||
"@appwrite.io/pink": "0.20.0",
|
||||
"@appwrite.io/pink-icons": "0.20.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@sentry/svelte": "^7.66.0",
|
||||
"@sentry/tracing": "^7.66.0",
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
<ul class="collapsible">
|
||||
<script lang="ts">
|
||||
export let fullWidth = false;
|
||||
</script>
|
||||
|
||||
<ul class="collapsible" class:u-width-full-line={fullWidth}>
|
||||
<slot />
|
||||
</ul>
|
||||
|
||||
@@ -4,40 +4,54 @@
|
||||
export let withIndentation = false;
|
||||
export let open = false;
|
||||
export let disabled = false;
|
||||
export let noContent = false;
|
||||
export let isInfo = false;
|
||||
export let gap = 16;
|
||||
</script>
|
||||
|
||||
<li class="collapsible-item">
|
||||
<details class="collapsible-wrapper" class:is-disabled={disabled} bind:open>
|
||||
<!-- svelte-ignore a11y-no-redundant-roles -->
|
||||
<summary
|
||||
class="collapsible-button u-position-relative"
|
||||
on:keyup={clickOnEnter}
|
||||
on:click
|
||||
role="button"
|
||||
tabindex="0">
|
||||
<slot name="beforetitle" />
|
||||
<div>
|
||||
<span class="text" class:u-color-text-disabled={disabled}
|
||||
><slot name="title" /></span>
|
||||
{#if $$slots.subtitle}
|
||||
<span class="collapsible-button-optional" class:u-color-text-disabled={disabled}
|
||||
><slot name="subtitle" /></span>
|
||||
{/if}
|
||||
<li class="collapsible-item" class:is-info={isInfo}>
|
||||
{#if noContent}
|
||||
<div class="collapsible-wrapper">
|
||||
<div class={`collapsible-button u-gap-${gap}`}>
|
||||
<slot />
|
||||
</div>
|
||||
<div class="icon">
|
||||
<span
|
||||
class="icon-cheveron-down u-font-size-20"
|
||||
class:u-color-text-disabled={disabled}
|
||||
aria-hidden="true" />
|
||||
</div>
|
||||
</summary>
|
||||
<div
|
||||
class="collapsible-content"
|
||||
class:u-margin-block-start-8={withIndentation}
|
||||
class:u-padding-inline-32={withIndentation}>
|
||||
<slot />
|
||||
</div>
|
||||
</details>
|
||||
{:else}
|
||||
<details class="collapsible-wrapper" class:is-disabled={disabled} bind:open>
|
||||
<!-- svelte-ignore a11y-no-redundant-roles -->
|
||||
<summary
|
||||
class={`collapsible-button u-position-relative u-gap-${gap}`}
|
||||
on:keyup={clickOnEnter}
|
||||
on:click
|
||||
role="button"
|
||||
tabindex="0">
|
||||
<slot name="beforetitle" />
|
||||
<div>
|
||||
<span class="text" class:u-color-text-disabled={disabled}>
|
||||
<slot name="title" /></span>
|
||||
{#if $$slots.subtitle}
|
||||
<span
|
||||
class="collapsible-button-optional"
|
||||
class:u-color-text-disabled={disabled}><slot name="subtitle" /></span>
|
||||
{/if}
|
||||
</div>
|
||||
<slot name="end">
|
||||
<div class="icon">
|
||||
<span
|
||||
class="icon-cheveron-down u-font-size-20"
|
||||
class:u-color-text-disabled={disabled}
|
||||
aria-hidden="true" />
|
||||
</div>
|
||||
</slot>
|
||||
</summary>
|
||||
<div
|
||||
class="collapsible-content"
|
||||
class:u-margin-block-start-8={withIndentation}
|
||||
class:u-padding-inline-32={withIndentation}>
|
||||
<slot />
|
||||
</div>
|
||||
</details>
|
||||
{/if}
|
||||
</li>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
class:u-flex={right}
|
||||
class:u-main-end={right}
|
||||
data-title={title}
|
||||
role="cell">
|
||||
role="cell"
|
||||
data-private>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { Trim } from '$lib/components';
|
||||
export let title: string;
|
||||
import Cell from './cell.svelte';
|
||||
export let title = '';
|
||||
export let showOverflow = false;
|
||||
export let onlyDesktop = false;
|
||||
export let width: number = null;
|
||||
export let right = false;
|
||||
let className = '';
|
||||
export { className as class };
|
||||
export let style = '';
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="table-col"
|
||||
style:--p-col-width={width?.toString()}
|
||||
class:is-only-desktop={onlyDesktop}
|
||||
class:u-overflow-visible={showOverflow}
|
||||
data-title={title}
|
||||
role="cell"
|
||||
data-private>
|
||||
<Cell {title} {showOverflow} {onlyDesktop} {width} {right} class={className} {style}>
|
||||
<Trim><slot /></Trim>
|
||||
</div>
|
||||
</Cell>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { Box, CardGrid, EyebrowHeading, Heading } from '$lib/components';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { CardGrid, Collapsible, CollapsibleItem, Heading } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import { plansInfo, tierToPlan } from '$lib/stores/billing';
|
||||
@@ -16,8 +15,10 @@
|
||||
import { humanFileSize } from '$lib/helpers/sizeConvertion';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
|
||||
let currentInvoice: Invoice;
|
||||
let extraMembers = 0;
|
||||
const today = new Date();
|
||||
onMount(async () => {
|
||||
const invoices = await sdk.forConsole.billing.listInvoices($organization.$id, [
|
||||
@@ -25,10 +26,13 @@
|
||||
Query.orderDesc('$createdAt')
|
||||
]);
|
||||
currentInvoice = invoices.invoices[0];
|
||||
const members = await sdk.forConsole.teams.listMemberships($organization.$id, []);
|
||||
extraMembers = members.total > 1 ? members.total - 1 : 0;
|
||||
});
|
||||
|
||||
$: currentPlan = $plansInfo?.get($organization?.billingPlan);
|
||||
$: extraUsage = currentInvoice?.amount - currentPlan?.price;
|
||||
$: extraUsage = (currentInvoice?.amount ?? 0) - (currentPlan?.price ?? 0);
|
||||
$: extraAddons = currentInvoice?.usage?.length ?? 0;
|
||||
$: isTrial =
|
||||
new Date($organization?.billingStartDate).getTime() - today.getTime() > 0 &&
|
||||
$plansInfo.get($organization.billingPlan)?.trialDays;
|
||||
@@ -36,102 +40,119 @@
|
||||
|
||||
{#if $organization}
|
||||
<CardGrid>
|
||||
<Heading tag="h2" size="6">Overview</Heading>
|
||||
<Heading tag="h2" size="6">Payment estimates</Heading>
|
||||
|
||||
<p class="text">
|
||||
A summary of the current plan. For more information on Appwrite plans, <a
|
||||
type="button"
|
||||
class="link"
|
||||
href="https://appwrite.io/pricing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
view our pricing page.
|
||||
</a>
|
||||
A breakdown of your estimated upcoming payment for the current billing period. Totals
|
||||
displayed exclude accumulated credits.
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<Box class="u-flex-vertical u-gap-8">
|
||||
<div class="u-flex u-main-space-between u-cross-center">
|
||||
<div class="u-flex u-gap-8 u-cross-center">
|
||||
<h6 class="body-text-1 u-bold u-trim-1">
|
||||
{tierToPlan($organization?.billingPlan)?.name} plan
|
||||
</h6>
|
||||
{#if $organization?.billingPlan !== BillingPlan.STARTER && isTrial}
|
||||
<Pill>TRIAL</Pill>
|
||||
{/if}
|
||||
<p class="text u-bold">
|
||||
Billing period: {toLocaleDate($organization?.billingCurrentInvoiceDate)} - {toLocaleDate(
|
||||
$organization?.billingNextInvoiceDate
|
||||
)}
|
||||
</p>
|
||||
<Collapsible>
|
||||
<CollapsibleItem noContent>
|
||||
<span class="body-text-2">
|
||||
{tierToPlan($organization?.billingPlan)?.name} plan</span>
|
||||
<div class="body-text-2 u-margin-inline-start-auto">
|
||||
{isTrial ? formatCurrency(0) : formatCurrency(currentPlan?.price)}
|
||||
</div>
|
||||
</CollapsibleItem>
|
||||
{#if $organization?.billingPlan !== BillingPlan.STARTER && extraUsage}
|
||||
<CollapsibleItem isInfo gap={8}>
|
||||
<svelte:fragment slot="beforetitle">
|
||||
<span class="body-text-2"><b>Add-ons</b></span><span class="inline-tag"
|
||||
>{extraMembers ? extraAddons + 1 : extraAddons}</span>
|
||||
<div class="icon">
|
||||
<span class="icon-cheveron-down" aria-hidden="true"></span>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="end">
|
||||
<div class="body-text-2 u-margin-inline-start-auto">
|
||||
<b>
|
||||
{formatCurrency(extraUsage >= 0 ? extraUsage : 0)}
|
||||
</b>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
<p class="text">
|
||||
{#if !extraUsage}
|
||||
<span class="text u-color-text-gray">Total to-date:</span>
|
||||
{/if}
|
||||
<span class="body-text-1">
|
||||
{isTrial ? formatCurrency(0) : formatCurrency(currentPlan?.price)}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
{#if currentInvoice?.usage?.length && $organization?.billingPlan !== BillingPlan.STARTER && !isTrial}
|
||||
{@const extraMembers = currentInvoice.usage.find((u) => u.name === 'members')}
|
||||
{#if extraMembers}
|
||||
<div class="u-margin-block-start-24">
|
||||
<EyebrowHeading tag="h6" size={3}>Additional members</EyebrowHeading>
|
||||
<ul>
|
||||
<li class="u-flex u-main-space-between u-margin-block-start-8">
|
||||
<p class="text u-color-text-gray">
|
||||
<span> {extraMembers.value}</span>
|
||||
{extraMembers.name}
|
||||
</p>
|
||||
<p class="text">{formatCurrency(extraMembers.amount)}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="u-margin-block-start-24">
|
||||
<EyebrowHeading tag="h6" size={3}>Addons</EyebrowHeading>
|
||||
<ul>
|
||||
{#each currentInvoice.usage as excess}
|
||||
{#if ['storage', 'bandwidth'].includes(excess.name)}
|
||||
{@const excessValue = humanFileSize(excess.value)}
|
||||
<li class="u-flex u-main-space-between u-margin-block-start-8">
|
||||
<p class="text u-color-text-gray">
|
||||
<span
|
||||
title={formatNumberWithCommas(excess.value) +
|
||||
{#if extraMembers}
|
||||
<li class="u-flex-vertical u-gap-4 u-padding-block-8">
|
||||
<div class="u-flex u-gap-4">
|
||||
<h5 class="body-text-2 u-stretch">Additional members</h5>
|
||||
<div>
|
||||
{formatCurrency(
|
||||
extraMembers * currentPlan.addons.member.price
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="u-flex u-gap-4">
|
||||
<h5 class="body-text-2 u-stretch u-color-text-offline">
|
||||
{extraMembers}
|
||||
</h5>
|
||||
</div>
|
||||
</li>
|
||||
{/if}
|
||||
{#if currentInvoice?.usage}
|
||||
{#each currentInvoice.usage as excess, i}
|
||||
<li
|
||||
class="u-flex-vertical u-gap-4 {extraMembers
|
||||
? 'u-padding-block-8'
|
||||
: 'u-padding-block-start-8'}"
|
||||
class:u-sep-block-start={i > 0 || extraMembers}>
|
||||
{#if ['storage', 'bandwidth'].includes(excess.name)}
|
||||
{@const excessValue = humanFileSize(excess.value)}
|
||||
<div class="u-flex u-main-space-between">
|
||||
<h5 class="body-text-2 u-stretch u-capitalize">
|
||||
{excess.name}
|
||||
</h5>
|
||||
{formatCurrency(excess.amount)}
|
||||
</div>
|
||||
<h5
|
||||
class="body-text-2 u-stretch u-color-text-offline"
|
||||
title={formatNumberWithCommas(excess.value ?? 0) +
|
||||
'bytes'}>
|
||||
{excessValue.value}{excessValue.unit}
|
||||
</span>
|
||||
{excess.name}
|
||||
</p>
|
||||
<p class="text">{formatCurrency(excess.amount)}</p>
|
||||
{excessValue.value ?? 0}{excessValue.unit}
|
||||
</h5>
|
||||
{/if}
|
||||
{#if ['users', 'executions'].includes(excess.name)}
|
||||
<div class="u-flex u-main-space-between">
|
||||
<h5 class="body-text-2 u-stretch u-capitalize">
|
||||
{excess.name}
|
||||
</h5>
|
||||
{formatCurrency(excess.amount)}
|
||||
</div>
|
||||
<h5
|
||||
class="body-text-2 u-stretch u-color-text-offline"
|
||||
title={formatNumberWithCommas(excess.value)}>
|
||||
{abbreviateNumber(excess.value)}
|
||||
</h5>
|
||||
{/if}
|
||||
</li>
|
||||
{/if}
|
||||
{#if ['users', 'executions'].includes(excess.name)}
|
||||
<li class="u-flex u-main-space-between u-margin-block-start-8">
|
||||
<p class="text u-color-text-gray">
|
||||
<span title={formatNumberWithCommas(excess.value)}
|
||||
>{abbreviateNumber(excess.value)}</span>
|
||||
{excess.name}
|
||||
</p>
|
||||
<p class="text">{formatCurrency(excess.amount)}</p>
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
<li class="u-flex u-main-space-between u-margin-block-start-16">
|
||||
<p class="body-text-1 u-bold">Total to-date:</p>
|
||||
<p class="body-text-1 u-bold">
|
||||
{formatCurrency(currentInvoice?.amount ?? 0)}
|
||||
</p>
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
</CollapsibleItem>
|
||||
{/if}
|
||||
</Box>
|
||||
<div class="u-flex u-main-space-between u-cross-center">
|
||||
<p class="body-text-1 u-bold">Billing period:</p>
|
||||
<p class="text">
|
||||
{toLocaleDate($organization?.billingCurrentInvoiceDate)} - {toLocaleDate(
|
||||
$organization?.billingNextInvoiceDate
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<CollapsibleItem noContent gap={4}>
|
||||
<span class="body-text-2">Current total (USD)</span>
|
||||
<span class="tooltip u-cross-center" aria-label="total info">
|
||||
<span
|
||||
class="icon-info"
|
||||
aria-hidden="true"
|
||||
use:tooltip={{
|
||||
content:
|
||||
'Totals displayed are estimates updated every 24 hours and may not accurately reflect your invoice.'
|
||||
}}></span>
|
||||
</span>
|
||||
<div class="body-text-2 u-margin-inline-start-auto">
|
||||
{formatCurrency(currentInvoice?.amount ?? 0)}
|
||||
</div>
|
||||
</CollapsibleItem>
|
||||
</Collapsible>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
{#if $organization?.billingPlan === BillingPlan.STARTER}
|
||||
|
||||
Reference in New Issue
Block a user