Skip to content

Commit

Permalink
Add pricing
Browse files Browse the repository at this point in the history
  • Loading branch information
pontusab committed Jan 8, 2025
1 parent 92b999a commit e53ec50
Show file tree
Hide file tree
Showing 23 changed files with 548 additions and 11 deletions.
4 changes: 3 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"dev": "next dev --turbo",
"build": "next build",
"start": "next start",
"lint": "next lint",
Expand All @@ -16,6 +16,7 @@
"dependencies": {
"@hookform/resolvers": "^3.10.0",
"@libsql/client": "^0.14.0",
"@number-flow/react": "^0.5.1",
"@openpanel/nextjs": "^1.0.7",
"@paralleldrive/cuid2": "^2.2.2",
"@radix-ui/react-alert-dialog": "^1.1.4",
Expand All @@ -27,6 +28,7 @@
"@radix-ui/react-popover": "^1.1.4",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slider": "^1.2.2",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.2",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/[locale]/(marketing)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default function Page() {
<div>
<Hero />

<div className="space-y-16">
<div className="space-y-16 max-w-screen-lg mx-auto">
<Companies />
<DottedSeparator />
<Features />
Expand Down
12 changes: 11 additions & 1 deletion apps/web/src/app/[locale]/(marketing)/pricing/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
import { DottedSeparator } from "@/components/dotted-separator";
import { Info } from "@/components/info";
import { Pricing } from "@/components/pricing";

export default function Page() {
return <div>Pricing</div>;
return (
<div className="space-y-12 max-w-screen-lg mx-auto">
<Pricing />
<DottedSeparator />
<Info />
</div>
);
}
12 changes: 8 additions & 4 deletions apps/web/src/components/change-language.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,22 @@ export function ChangeLanguage() {
<DropdownMenu>
<DropdownMenuTrigger
type="button"
className="flex items-center gap-2 text-secondary outline-none"
className="flex items-center gap-2 text-secondary outline-none uppercase text-xs font-medium"
>
{currentLocale}
[{currentLocale}]
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
sideOffset={10}
className="max-h-[300px] overflow-y-auto"
>
{locales.map((locale) => (
// @ts-ignore
<DropdownMenuItem key={locale} onClick={() => changeLocale(locale)}>
<DropdownMenuItem
key={locale}
// @ts-ignore
onClick={() => changeLocale(locale)}
className="uppercase text-xs"
>
{locale}
</DropdownMenuItem>
))}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/dotted-separator.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export function DottedSeparator() {
return <div className="bg-dotted h-[60px] w-full" />;
return <div className="bg-dotted h-[45px] w-full" />;
}
10 changes: 8 additions & 2 deletions apps/web/src/components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,24 @@ import { SignIn } from "@/components/sign-in";
import { cn } from "@/lib/utils";
import { useI18n } from "@/locales/client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Suspense } from "react";
import { ChangeLanguage } from "./change-language";
import { GithubStars } from "./github-stars";

export function Header() {
const t = useI18n();

const pathname = usePathname();
console.log(pathname);
const links = [
{ href: "/pricing", label: t("header.pricing") },
{ href: "https://git.new/languine", label: t("header.docs") },
{
component: <SignIn />,
className: "text-primary",
className:
pathname.split("/").length === 2
? "text-primary"
: "text-secondary hover:text-primary",
},
];

Expand Down Expand Up @@ -54,6 +59,7 @@ export function Header() {
className={cn(
"text-secondary hover:text-primary transition-colors hidden md:block",
link.className,
pathname?.endsWith(link.href) && "text-primary",
)}
key={link.href}
>
Expand Down
65 changes: 65 additions & 0 deletions apps/web/src/components/pricing-slider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Slider } from "@/components/ui/slider";
import NumberFlow from "@number-flow/react";

export function PricingSlider({
value,
setValue,
}: { value: number[]; setValue: (value: number[]) => void }) {
const getKeysForPrice = (price: number) => {
if (price <= 49) return 2000;

const tiers = Math.floor((price - 49) / 49);

if (getKeysForPrice(49 + (tiers - 1) * 49) <= 50000) {
return 2000 + tiers * 4000;
}

const tiersTo50k = 12;
const remainingTiers = tiers - tiersTo50k;
return 50000 + remainingTiers * 8000;
};

const getMaxPrice = () => {
let price = 49;
while (getKeysForPrice(price) < 250000) {
price += 49;
}
return price;
};

const maxPrice = getMaxPrice();

return (
<div className="mt-8">
<div className="relative mb-6">
<div
className="absolute -top-12 left-0 transform -translate-x-1/2 bg-background font-medium text-primary text-[11px] px-3 py-1 rounded-full border border-border text-center whitespace-nowrap"
style={{ left: `${(value[0] / maxPrice) * 100}%` }}
>
{Math.floor(getKeysForPrice(value[0]) / 1000)}k keys
</div>
<Slider
value={value}
onValueChange={(newValue) =>
setValue(newValue.map((v) => Math.max(49, v)))
}
step={49}
min={0}
max={maxPrice}
/>
</div>
<NumberFlow
value={value[0]}
defaultValue={49}
className="font-mono text-2xl"
locales="en-US"
format={{
style: "currency",
currency: "USD",
trailingZeroDisplay: "stripIfInteger",
}}
suffix="/mon"
/>
</div>
);
}
110 changes: 110 additions & 0 deletions apps/web/src/components/pricing.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"use client";

import { useI18n } from "@/locales/client";
import Link from "next/link";
import { useState } from "react";
import { MdCheck } from "react-icons/md";
import { PricingSlider } from "./pricing-slider";
import { OutlinedButton } from "./ui/outlined-button";

export function Pricing() {
const t = useI18n();
const [value, setValue] = useState([49]);

return (
<div className="space-y-12 pt-12 md:pt-28">
<h1 className="text-3xl">{t("pricing.title")}</h1>
<div className="border border-primary">
<div className="grid grid-cols-1 md:grid-cols-2 divide-y md:divide-y-0 md:divide-x divide-primary">
<div className="p-12 relative">
<div className="absolute -top-4 left-6 bg-background px-4 py-1">
<h3 className="text-sm font-medium uppercase">
{t("pricing.free.title")}
</h3>
</div>
<ul className="space-y-2">
<li className="flex items-center gap-2 text-sm text-secondary">
<MdCheck className="w-4 h-4 text-primary" />
<span>{t("pricing.free.features.unlimited_projects")}</span>
</li>
<li className="flex items-center gap-2 text-sm text-secondary">
<MdCheck className="w-4 h-4 text-primary" />
<span>{t("pricing.free.features.fine_tuning")}</span>
</li>
<li className="flex items-center gap-2 text-sm text-secondary">
<MdCheck className="w-4 h-4 text-primary" />
<span>{t("pricing.free.features.overrides")}</span>
</li>
<li className="flex items-center gap-2 text-sm text-secondary">
<MdCheck className="w-4 h-4 text-primary" />
<span>{t("pricing.free.features.analytics")}</span>
</li>
<li className="flex items-center gap-2 text-sm text-secondary">
<MdCheck className="w-4 h-4 text-primary" />
<span>{t("pricing.free.features.context_memory")}</span>
</li>
<li className="flex items-center gap-2 text-sm text-secondary">
<MdCheck className="w-4 h-4 text-primary" />
<span>{t("pricing.free.features.community_support")}</span>
</li>
</ul>
</div>

<div className="p-12 relative">
<div className="absolute -top-4 left-6 bg-background px-4 py-1">
<h3 className="text-sm font-medium uppercase">
{t("pricing.pro.title")}
</h3>
</div>
<ul className="space-y-2">
<li className="flex items-center gap-2 text-sm text-secondary mb-4">
<span>{t("pricing.pro.includes_free")}</span>
</li>
<li className="flex items-center gap-2 text-sm text-secondary">
<MdCheck className="w-4 h-4 text-primary" />
<span>{t("pricing.pro.features.github_action")}</span>
</li>
<li className="flex items-center gap-2 text-sm text-secondary">
<MdCheck className="w-4 h-4 text-primary" />
<span>{t("pricing.pro.features.latest_features")}</span>
</li>
<li className="flex items-center gap-2 text-sm text-secondary">
<MdCheck className="w-4 h-4 text-primary" />
<span>{t("pricing.pro.features.priority_support")}</span>
</li>
</ul>
</div>
</div>

<div className="grid grid-cols-1 md:grid-cols-2 divide-y md:divide-y-0 md:divide-x divide-primary border-t border-primary">
<div className="p-12 pt-20">
<span className="text-sm text-secondary">
{t("pricing.free.keys_limit")}
</span>
<h3 className="text-xl font-medium mb-6 mt-2">
{t("pricing.free.price")}
</h3>

<div className="mt-4">
<Link href="/login?plan=free">
<OutlinedButton variant="secondary">
{t("pricing.cta")}
</OutlinedButton>
</Link>
</div>
</div>

<div className="p-12 pt-12">
<PricingSlider value={value} setValue={setValue} />

<div className="mt-4">
<Link href={`/login?plan=pro&tier=${value[0]}`}>
<OutlinedButton>{t("pricing.cta")}</OutlinedButton>
</Link>
</div>
</div>
</div>
</div>
</div>
);
}
2 changes: 1 addition & 1 deletion apps/web/src/components/sign-in.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function SignIn() {
const { data: session } = authClient.useSession();

return (
<Link href="/login" className="text-primary">
<Link href="/login">
{session ? t("header.goToApp") : t("header.signIn")}
</Link>
);
Expand Down
28 changes: 28 additions & 0 deletions apps/web/src/components/ui/slider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use client";

import * as SliderPrimitive from "@radix-ui/react-slider";
import * as React from "react";

import { cn } from "@/lib/utils";

const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
"relative flex w-full touch-none select-none items-center",
className,
)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;

export { Slider };
26 changes: 26 additions & 0 deletions apps/web/src/locales/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,4 +273,30 @@ export default {
description: "تكييف المحتوى للملاءمة الثقافية",
},
},
pricing: {
title: "تسعير بسيط",
free: {
title: "مجاني (100 مفتاح)",
price: "مجاني",
keys_limit: "حتى 100 مفتاح",
features: {
unlimited_projects: "مشاريع غير محدودة",
fine_tuning: "خيارات الضبط الدقيق",
overrides: "تجاوزات الترجمة",
analytics: "التحليلات",
context_memory: "ذاكرة السياق",
community_support: "دعم المجتمع",
},
},
pro: {
title: "احترافي",
includes_free: "كل ما في النسخة المجانية، بالإضافة إلى:",
features: {
github_action: "تكامل GitHub Action",
latest_features: "وصول مبكر لأحدث الميزات",
priority_support: "دعم ذو أولوية",
},
},
cta: "ابدأ الأتمتة",
},
} as const;
26 changes: 26 additions & 0 deletions apps/web/src/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,4 +284,30 @@ export default {
description: "Passen Sie Inhalte für kulturelle Angemessenheit an",
},
},
pricing: {
title: "Einfache Preisgestaltung",
free: {
title: "Kostenlos (100 Schlüssel)",
price: "Kostenlos",
keys_limit: "Bis zu 100 Schlüssel",
features: {
unlimited_projects: "Unbegrenzte Projekte",
fine_tuning: "Feinabstimmungsoptionen",
overrides: "Übersetzungsüberschreibungen",
analytics: "Analysen",
context_memory: "Kontextspeicher",
community_support: "Community-Support",
},
},
pro: {
title: "Pro",
includes_free: "Alles aus der kostenlosen Version, plus:",
features: {
github_action: "GitHub Action Integration",
latest_features: "Frühzeitiger Zugang zu neuen Funktionen",
priority_support: "Prioritäts-Support",
},
},
cta: "Automatisierung starten",
},
} as const;
Loading

0 comments on commit e53ec50

Please sign in to comment.