aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-07-19 17:49:48 +0000
committerMohamed Bassem <me@mbassem.com>2025-08-04 02:51:16 +0000
commit141f411b714b88516071d7dba73cec54f32d3a23 (patch)
treeb449e8e51abaa8045bda67bdf3521c4320aed0cd
parenteb720c486fefebc679883c54219fa55063b1dcc6 (diff)
downloadkarakeep-141f411b714b88516071d7dba73cec54f32d3a23.tar.zst
feat(landing): The pricing page
-rw-r--r--apps/landing/package.json1
-rw-r--r--apps/landing/src/App.tsx17
-rw-r--r--apps/landing/src/Homepage.tsx44
-rw-r--r--apps/landing/src/Navbar.tsx59
-rw-r--r--apps/landing/src/Pricing.tsx224
-rw-r--r--apps/landing/src/constants.ts4
-rw-r--r--pnpm-lock.yaml38
7 files changed, 336 insertions, 51 deletions
diff --git a/apps/landing/package.json b/apps/landing/package.json
index 3343a542..e6db4d83 100644
--- a/apps/landing/package.json
+++ b/apps/landing/package.json
@@ -22,6 +22,7 @@
"lucide-react": "^0.501.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-router": "^7.7.1",
"react-select": "^5.8.0",
"sharp": "^0.33.3",
"tailwind-merge": "^2.2.1",
diff --git a/apps/landing/src/App.tsx b/apps/landing/src/App.tsx
index ddf92e32..45a019c5 100644
--- a/apps/landing/src/App.tsx
+++ b/apps/landing/src/App.tsx
@@ -1,13 +1,18 @@
import Homepage from "@/src/Homepage";
+import Pricing from "@/src/Pricing";
import Privacy from "@/src/Privacy";
+import { BrowserRouter, Route, Routes } from "react-router";
import "@karakeep/tailwind-config/globals.css";
export default function App() {
- // Poor man router
- if (window.location.pathname === "/privacy") {
- return <Privacy />;
- }
-
- return <Homepage />;
+ return (
+ <BrowserRouter>
+ <Routes>
+ <Route path="/" element={<Homepage />} />
+ <Route path="/pricing" element={<Pricing />} />
+ <Route path="/privacy" element={<Privacy />} />
+ </Routes>
+ </BrowserRouter>
+ );
}
diff --git a/apps/landing/src/Homepage.tsx b/apps/landing/src/Homepage.tsx
index 7ab7fdfd..f88f35af 100644
--- a/apps/landing/src/Homepage.tsx
+++ b/apps/landing/src/Homepage.tsx
@@ -12,16 +12,13 @@ import {
WalletCards,
} from "lucide-react";
+import { DEMO_LINK, DOCS_LINK, GITHUB_LINK } from "./constants";
+import NavBar from "./Navbar";
import appStoreBadge from "/app-store-badge.png?url";
import chromeExtensionBadge from "/chrome-extension-badge.png?url";
import firefoxAddonBadge from "/firefox-addon.png?url";
import playStoreBadge from "/google-play-badge.webp?url";
import screenshot from "/hero.webp?url";
-import Logo from "/icons/karakeep-full.svg?url";
-
-const GITHUB_LINK = "https://github.com/karakeep-app/karakeep";
-const DOCS_LINK = "https://docs.karakeep.app";
-const DEMO_LINK = "https://try.karakeep.app";
const platforms = [
{
@@ -93,43 +90,6 @@ const featuresList = [
const currentYear = new Date().getFullYear();
-function NavBar() {
- return (
- <div className="flex justify-between px-3 py-4">
- <img src={Logo} alt="logo" className="w-36" />
- <div className="hidden items-center gap-6 sm:flex">
- <a
- href={DOCS_LINK}
- target="_blank"
- className="flex justify-center gap-2 text-center"
- rel="noreferrer"
- >
- Docs
- </a>
- <a
- href={GITHUB_LINK}
- target="_blank"
- className="flex justify-center gap-2 text-center"
- rel="noreferrer"
- >
- GitHub
- </a>
- <a
- href={DEMO_LINK}
- target="_blank"
- className={cn(
- "text flex h-full w-28 gap-2",
- buttonVariants({ variant: "default" }),
- )}
- rel="noreferrer"
- >
- Try Demo
- </a>
- </div>
- </div>
- );
-}
-
function Hero() {
return (
<div className="mt-10 flex flex-grow flex-col items-center justify-center gap-6 sm:mt-20">
diff --git a/apps/landing/src/Navbar.tsx b/apps/landing/src/Navbar.tsx
new file mode 100644
index 00000000..6d252db4
--- /dev/null
+++ b/apps/landing/src/Navbar.tsx
@@ -0,0 +1,59 @@
+import { buttonVariants } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+import { Link } from "react-router";
+
+import { DEMO_LINK, DOCS_LINK, GITHUB_LINK } from "./constants";
+import Logo from "/icons/karakeep-full.svg?url";
+
+export default function NavBar() {
+ return (
+ <div className="flex justify-between px-3 py-4">
+ <Link to="/">
+ <img src={Logo} alt="logo" className="w-36" />
+ </Link>
+ <div className="hidden items-center gap-6 sm:flex">
+ <Link to="/pricing" className="flex justify-center gap-2 text-center">
+ Pricing
+ </Link>
+ <a
+ href={DOCS_LINK}
+ target="_blank"
+ className="flex justify-center gap-2 text-center"
+ rel="noreferrer"
+ >
+ Docs
+ </a>
+ <a
+ href={GITHUB_LINK}
+ target="_blank"
+ className="flex justify-center gap-2 text-center"
+ rel="noreferrer"
+ >
+ GitHub
+ </a>
+ <a
+ href="https://cloud.karakeep.app"
+ target="_blank"
+ className={cn(
+ "text flex h-full w-20 gap-2",
+ buttonVariants({ variant: "outline" }),
+ )}
+ rel="noreferrer"
+ >
+ Login
+ </a>
+ <a
+ href={DEMO_LINK}
+ target="_blank"
+ className={cn(
+ "text flex h-full w-28 gap-2",
+ buttonVariants({ variant: "default" }),
+ )}
+ rel="noreferrer"
+ >
+ Try Demo
+ </a>
+ </div>
+ </div>
+ );
+}
diff --git a/apps/landing/src/Pricing.tsx b/apps/landing/src/Pricing.tsx
new file mode 100644
index 00000000..9a1c06f0
--- /dev/null
+++ b/apps/landing/src/Pricing.tsx
@@ -0,0 +1,224 @@
+import { buttonVariants } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+import { Check, ExternalLink } from "lucide-react";
+
+import { DOCS_LINK, GITHUB_LINK, WAITLIST_LINK } from "./constants";
+import NavBar from "./Navbar";
+
+const pricingTiers = [
+ {
+ name: "Free",
+ price: "$0",
+ period: "",
+ description: "Trying Karakeep out",
+ features: [
+ "10 bookmarks",
+ "20MB storage",
+ "Mobile & web apps",
+ "Browser extensions",
+ ],
+ buttonText: "Join Waitlist",
+ buttonVariant: "outline" as const,
+ popular: false,
+ },
+ {
+ name: "Pro",
+ price: "$4",
+ period: "per month",
+ description: "For serious bookmark collectors",
+ features: [
+ "50,000 bookmarks",
+ "50GB storage",
+ "AI-powered tagging",
+ "Full-text search",
+ "Mobile & web apps",
+ "Browser extensions",
+ ],
+ buttonText: "Join Waitlist",
+ buttonVariant: "default" as const,
+ popular: true,
+ },
+ {
+ name: "Self-Hosted",
+ price: "Free",
+ period: "forever",
+ description: "Complete control and privacy",
+ features: [
+ "Unlimited bookmarks",
+ "Unlimited storage",
+ "Complete data control",
+ "Mobile & web apps",
+ "Browser extensions",
+ "Community support",
+ ],
+ buttonText: "View on GitHub",
+ buttonVariant: "outline" as const,
+ popular: false,
+ isGitHub: true,
+ },
+];
+
+function PricingHeader() {
+ return (
+ <div className="text-center">
+ <h1 className="text-4xl font-bold sm:text-6xl">
+ Simple{" "}
+ <span className="bg-gradient-to-r from-purple-600 to-red-600 bg-clip-text text-transparent">
+ Pricing
+ </span>
+ </h1>
+ <p className="mt-4 text-lg text-gray-600">
+ Choose the plan that works best for you
+ </p>
+ </div>
+ );
+}
+
+function PricingCards() {
+ return (
+ <div className="mx-auto mt-16 grid max-w-6xl grid-cols-1 gap-8 px-6 md:grid-cols-3">
+ {pricingTiers.map((tier) => (
+ <div
+ key={tier.name}
+ className={cn(
+ "relative rounded-2xl border bg-white p-8 shadow-sm",
+ tier.popular && "border-purple-500 shadow-lg",
+ )}
+ >
+ <div className="text-center">
+ <h3 className="text-xl font-semibold">{tier.name}</h3>
+ <div className="mt-4 flex items-baseline justify-center">
+ <span className="text-4xl font-bold">{tier.price}</span>
+ {tier.period && (
+ <span className="ml-1 text-gray-500">/{tier.period}</span>
+ )}
+ </div>
+ <p className="mt-2 text-gray-600">{tier.description}</p>
+ </div>
+
+ <ul className="mt-8 space-y-3">
+ {tier.features.map((feature) => (
+ <li key={feature} className="flex items-center">
+ <Check className="h-5 w-5 text-green-500" />
+ <span className="ml-3 text-gray-700">{feature}</span>
+ </li>
+ ))}
+ </ul>
+
+ <div className="mt-8">
+ {tier.isGitHub ? (
+ <a
+ href={GITHUB_LINK}
+ target="_blank"
+ className={cn(
+ "flex w-full items-center justify-center gap-2",
+ buttonVariants({ variant: tier.buttonVariant, size: "lg" }),
+ )}
+ rel="noreferrer"
+ >
+ <ExternalLink className="h-4 w-4" />
+ {tier.buttonText}
+ </a>
+ ) : (
+ <a
+ href={WAITLIST_LINK}
+ target="_blank"
+ className={cn(
+ "flex w-full items-center justify-center",
+ buttonVariants({ variant: tier.buttonVariant, size: "lg" }),
+ )}
+ rel="noreferrer"
+ >
+ {tier.buttonText}
+ </a>
+ )}
+ </div>
+ </div>
+ ))}
+ </div>
+ );
+}
+
+function FAQ() {
+ const faqs = [
+ {
+ question: "What happens to my data if I cancel?",
+ answer:
+ "Your data will be available for 30 days after cancellation. You can export your bookmarks at any time.",
+ },
+ {
+ question: "Are there any restrictions in the self-hosted version?",
+ answer:
+ "No. The selhosted version is completely free, fully-featured, and open source. You just need to provide your own hosting infrastructure.",
+ },
+ {
+ question: "Do you offer refunds?",
+ answer: "Yes, we offer a 7-day money-back guarantee for all paid plans.",
+ },
+ ];
+
+ return (
+ <div className="mx-auto mt-24 max-w-4xl px-6">
+ <h2 className="text-center text-3xl font-bold">
+ Frequently Asked Questions
+ </h2>
+ <div className="mt-12 grid gap-8 md:grid-cols-2">
+ {faqs.map((faq) => (
+ <div key={faq.question}>
+ <h3 className="text-lg font-semibold">{faq.question}</h3>
+ <p className="mt-2 text-gray-600">{faq.answer}</p>
+ </div>
+ ))}
+ </div>
+ </div>
+ );
+}
+
+function Footer() {
+ const currentYear = new Date().getFullYear();
+
+ return (
+ <div className="mt-24 flex items-center justify-between bg-gray-100 px-10 py-6 text-sm">
+ <div>
+ © 2024-{currentYear}{" "}
+ <a href="https://localhostlabs.co.uk" target="_blank" rel="noreferrer">
+ Localhost Labs Ltd
+ </a>
+ </div>
+ <div className="flex items-center gap-6">
+ <a
+ href={DOCS_LINK}
+ target="_blank"
+ className="flex justify-center gap-2 text-center"
+ rel="noreferrer"
+ >
+ Docs
+ </a>
+ <a
+ href={GITHUB_LINK}
+ target="_blank"
+ className="flex justify-center gap-2 text-center"
+ rel="noreferrer"
+ >
+ GitHub
+ </a>
+ </div>
+ </div>
+ );
+}
+
+export default function Pricing() {
+ return (
+ <div className="min-h-screen bg-gray-50">
+ <div className="container mx-auto">
+ <NavBar />
+ <div className="py-16">
+ <PricingHeader />
+ <PricingCards />
+ <FAQ />
+ </div>
+ </div>
+ <Footer />
+ </div>
+ );
+}
diff --git a/apps/landing/src/constants.ts b/apps/landing/src/constants.ts
new file mode 100644
index 00000000..b75cecae
--- /dev/null
+++ b/apps/landing/src/constants.ts
@@ -0,0 +1,4 @@
+export const GITHUB_LINK = "https://github.com/karakeep-app/karakeep";
+export const DOCS_LINK = "https://docs.karakeep.app";
+export const DEMO_LINK = "https://try.karakeep.app";
+export const WAITLIST_LINK = "https://tally.so/r/wo8zzx";
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f178bf24..16ba2b01 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -219,6 +219,9 @@ importers:
react-dom:
specifier: ^18.3.1
version: 18.3.1(react@18.3.1)
+ react-router:
+ specifier: ^7.7.1
+ version: 7.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-select:
specifier: ^5.8.0
version: 5.8.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -644,7 +647,7 @@ importers:
version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
nuqs:
specifier: ^2.4.3
- version: 2.4.3(next@14.2.25(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1))(react-router-dom@6.22.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-router@6.22.1(react@18.3.1))(react@18.3.1)
+ version: 2.4.3(next@14.2.25(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1))(react-router-dom@6.22.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-router@7.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
prettier:
specifier: ^3.4.2
version: 3.4.2
@@ -6895,6 +6898,10 @@ packages:
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
engines: {node: '>= 0.6'}
+ cookie@1.0.2:
+ resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
+ engines: {node: '>=18'}
+
copy-anything@3.0.5:
resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==}
engines: {node: '>=12.13'}
@@ -12376,6 +12383,16 @@ packages:
peerDependencies:
react: '>=16.8'
+ react-router@7.7.1:
+ resolution: {integrity: sha512-jVKHXoWRIsD/qS6lvGveckwb862EekvapdHJN/cGmzw40KnJH5gg53ujOJ4qX6EKIK9LSBfFed/xiQ5yeXNrUA==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+
react-select@5.8.0:
resolution: {integrity: sha512-TfjLDo58XrhP6VG5M/Mi56Us0Yt8X7xD6cDybC7yoRMUNm7BGO7qk8J0TLQOua/prb8vUOtsfnXZwfm30HGsAA==}
peerDependencies:
@@ -12882,6 +12899,9 @@ packages:
server-only@0.0.1:
resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==}
+ set-cookie-parser@2.7.1:
+ resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
+
set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'}
@@ -22768,6 +22788,8 @@ snapshots:
cookie@0.7.2: {}
+ cookie@1.0.2: {}
+
copy-anything@3.0.5:
dependencies:
is-what: 4.1.16
@@ -27666,13 +27688,13 @@ snapshots:
nullthrows@1.1.1: {}
- nuqs@2.4.3(next@14.2.25(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1))(react-router-dom@6.22.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-router@6.22.1(react@18.3.1))(react@18.3.1):
+ nuqs@2.4.3(next@14.2.25(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1))(react-router-dom@6.22.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-router@7.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1):
dependencies:
mitt: 3.0.1
react: 18.3.1
optionalDependencies:
next: 14.2.25(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.1)
- react-router: 6.22.1(react@18.3.1)
+ react-router: 7.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-router-dom: 6.22.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
nwsapi@2.2.20: {}
@@ -29454,6 +29476,14 @@ snapshots:
'@remix-run/router': 1.15.1
react: 18.3.1
+ react-router@7.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ cookie: 1.0.2
+ react: 18.3.1
+ set-cookie-parser: 2.7.1
+ optionalDependencies:
+ react-dom: 18.3.1(react@18.3.1)
+
react-select@5.8.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@babel/runtime': 7.27.6
@@ -30174,6 +30204,8 @@ snapshots:
server-only@0.0.1: {}
+ set-cookie-parser@2.7.1: {}
+
set-function-length@1.2.2:
dependencies:
define-data-property: 1.1.4