Skip to content

Commit

Permalink
implement the trial summary popup
Browse files Browse the repository at this point in the history
  • Loading branch information
nilscox committed Jan 8, 2025
1 parent 4d87b67 commit 9f5e0a7
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 10 deletions.
1 change: 1 addition & 0 deletions src/components/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export { default as IconFilePlus } from 'lucide-static/icons/file-plus-2.svg?rea
export { default as IconFolders } from 'lucide-static/icons/folders.svg?react';
export { default as IconFullscreen } from 'lucide-static/icons/fullscreen.svg?react';
export { default as IconFolderCode } from 'lucide-static/icons/folder-code.svg?react';
export { default as IconGem } from 'lucide-static/icons/gem.svg?react';
export { default as IconGitBranch } from 'lucide-static/icons/git-branch.svg?react';
export { default as IconGitCommitHorizontal } from 'lucide-static/icons/git-commit-horizontal.svg?react';
export { default as IconGithub } from 'lucide-static/icons/github.svg?react';
Expand Down
10 changes: 8 additions & 2 deletions src/hooks/feature-flag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ import { useFeatureFlagEnabled } from 'posthog-js/react';
import { useMemo } from 'react';
import { z } from 'zod';

export function FeatureFlag({ feature, children }: { feature: string; children: React.ReactNode }) {
type FeatureFlagProps = {
feature: string;
fallback?: React.ReactNode;
children: React.ReactNode;
};

export function FeatureFlag({ feature, fallback = null, children }: FeatureFlagProps) {
const enabled = useFeatureFlag(feature);

if (!enabled) {
return null;
return fallback;
}

return children;
Expand Down
12 changes: 12 additions & 0 deletions src/intl/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1385,6 +1385,14 @@
},
"banner": {
"content": "Your trial is active for the next <strong>{days, plural, =1 {{days} day} other {{days} days}}</strong> or until your <strong>credit runs out</strong>. Enjoy exploring or <upgrade>upgrade to Pro</upgrade>!"
},
"summaryPopup": {
"currentPlan": "{plan} Plan",
"badge": "Trial",
"usage": "Usage",
"creditLeft": "Trial credit left",
"timeLeft": "{days, plural, =1 {{days} day} other {{days} days}} of trial left",
"cta": "Go to billing"
}
},
"layouts": {
Expand Down Expand Up @@ -1423,6 +1431,10 @@
"system": "System",
"logout": "Log out"
},
"organizationPlan": {
"currentPlan": "{plan} Plan",
"upgrade": "Upgrade now"
},
"estimatedCost": {
"currentPlan": "{plan} plan",
"free": "Free",
Expand Down
4 changes: 2 additions & 2 deletions src/layouts/main/estimated-costs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function EstimatedCosts() {
}

return (
<div className="mx-4 divide-y rounded-md border bg-neutral">
<>
<div className="row justify-between p-2 font-medium">
<div className="capitalize">
<T id="currentPlan" values={{ plan: organization.plan }} />
Expand All @@ -37,7 +37,7 @@ export function EstimatedCosts() {
{nextInvoiceQuery.isSuccess && <CostsDetails costs={getCosts(nextInvoiceQuery.data)} />}
</>
)}
</div>
</>
);
}

Expand Down
20 changes: 14 additions & 6 deletions src/layouts/main/main-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { GlobalAlert } from './global-alert';
import { HelpLinks } from './help-links';
import { Layout } from './layout';
import { Navigation } from './navigation';
import { OrganizationPlan } from './organization-plan';
import { OrganizationSwitcher } from './organization-switcher';
import { PlatformStatus } from './platform-status';
import { UserMenu } from './user-menu';
Expand Down Expand Up @@ -103,13 +104,20 @@ function Menu({ collapsed = false }: { collapsed?: boolean }) {

<Navigation collapsed={collapsed} />

{!collapsed && <EstimatedCosts />}

<UserMenu collapsed={collapsed} />
<div className="col gap-4">
{!collapsed && (
<div className="mx-4 divide-y rounded-md border bg-neutral">
<UserMenu collapsed={collapsed} />
<FeatureFlag feature="trial" fallback={<EstimatedCosts />}>
<OrganizationPlan />
</FeatureFlag>
</div>
)}

<div className="col gap-2">
<HelpLinks collapsed={collapsed} />
<PlatformStatus collapsed={collapsed} />
<div className="col gap-2">
<HelpLinks collapsed={collapsed} />
<PlatformStatus collapsed={collapsed} />
</div>
</div>
</div>
);
Expand Down
82 changes: 82 additions & 0 deletions src/layouts/main/organization-plan.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import clsx from 'clsx';
import { useState } from 'react';

import { Badge, Floating } from '@koyeb/design-system';
import { useOrganizationUnsafe } from 'src/api/hooks/session';
import { routes } from 'src/application/routes';
import { IconGem } from 'src/components/icons';
import { LinkButton } from 'src/components/link';
import { FeatureFlag } from 'src/hooks/feature-flag';
import { createTranslate, TranslateEnum } from 'src/intl/translate';
import { TrialSummaryPopup } from 'src/modules/trial/trial-summary-popup';

import { EstimatedCosts } from './estimated-costs';

const T = createTranslate('layouts.main.organizationPlan');

export function OrganizationPlan() {
const [open, setOpen] = useState(false);
const organization = useOrganizationUnsafe();

return (
<Floating
open={open}
setOpen={setOpen}
hover
strategy="fixed"
placement="right-end"
offset={8}
renderReference={(ref, props) => (
<div
ref={ref}
{...props}
className={clsx('col gap-4 px-3 py-2 text-start transition-colors', open && 'bg-muted/50')}
>
<div className="row items-center gap-2">
<div>
<IconGem className="size-6 text-dim" />
</div>

<div>
<T
id="currentPlan"
values={{ plan: organization && <TranslateEnum enum="plans" value={organization.plan} /> }}
/>
</div>

{organization?.trial && (
<FeatureFlag feature="trial">
<Badge size={1} color="green" className="ms-auto">
Trial
</Badge>
</FeatureFlag>
)}
</div>

<div>
<LinkButton
variant="outline"
size={1}
href={routes.organizationSettings.plans()}
className="w-full"
>
<T id="upgrade" />
</LinkButton>
</div>
</div>
)}
renderFloating={(ref, props) => (
<FeatureFlag
feature="trial"
fallback={
<div ref={ref} {...props} className="z-30 w-56 rounded-md border bg-popover">
<EstimatedCosts />
</div>
}
>
<TrialSummaryPopup ref={ref} onClose={() => setOpen(false)} className="z-30" {...props} />
</FeatureFlag>
)}
/>
);
}
60 changes: 60 additions & 0 deletions src/modules/trial/trial-summary-popup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import clsx from 'clsx';
import { forwardRef } from 'react';

import { Badge, ProgressBar } from '@koyeb/design-system';
import { useOrganization } from 'src/api/hooks/session';
import { routes } from 'src/application/routes';
import { LinkButton } from 'src/components/link';
import { createTranslate, TranslateEnum } from 'src/intl/translate';

const T = createTranslate('trial.summaryPopup');

type TrialSummaryPopupProps = React.ComponentProps<'div'> & {
onClose: () => void;
};

export const TrialSummaryPopup = forwardRef<HTMLDivElement, TrialSummaryPopupProps>(
function TrialSummaryPopup({ onClose, className, ...props }, ref) {
const organization = useOrganization();

return (
<div ref={ref} {...props} className={clsx('w-56 rounded-md border bg-popover', className)}>
<div className="row justify-between border-b p-3">
<T id="currentPlan" values={{ plan: <TranslateEnum enum="plans" value={organization.plan} /> }} />

<Badge size={1} color="green" className="ms-auto">
<T id="badge" />
</Badge>
</div>

<div className="col gap-3 p-3">
<div className="row justify-between text-dim">
<div>
<T id="usage" />
</div>
<div>$0.00</div>
</div>

<hr />

<div className="row justify-between">
<div className="font-medium">
<T id="creditLeft" />
</div>
<div className="text-green">$10.00</div>
</div>

<ProgressBar progress={1} label={false} />

<div className="text-center text-xs text-dim">
<T id="timeLeft" values={{ days: 7 }} />
</div>

<LinkButton color="gray" href={routes.organizationSettings.billing()} onClick={onClose}>
<T id="cta" />
</LinkButton>
</div>
</div>
);
},
);

0 comments on commit 9f5e0a7

Please sign in to comment.