From 141f411b714b88516071d7dba73cec54f32d3a23 Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sat, 19 Jul 2025 17:49:48 +0000 Subject: feat(landing): The pricing page --- apps/landing/package.json | 1 + apps/landing/src/App.tsx | 17 ++-- apps/landing/src/Homepage.tsx | 44 +-------- apps/landing/src/Navbar.tsx | 59 +++++++++++ apps/landing/src/Pricing.tsx | 224 ++++++++++++++++++++++++++++++++++++++++++ apps/landing/src/constants.ts | 4 + pnpm-lock.yaml | 38 ++++++- 7 files changed, 336 insertions(+), 51 deletions(-) create mode 100644 apps/landing/src/Navbar.tsx create mode 100644 apps/landing/src/Pricing.tsx create mode 100644 apps/landing/src/constants.ts 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 ; - } - - return ; + return ( + + + } /> + } /> + } /> + + + ); } 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 ( -
- logo -
- - Docs - - - GitHub - - - Try Demo - -
-
- ); -} - function Hero() { return (
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 ( +
+ + logo + + +
+ ); +} 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 ( +
+

+ Simple{" "} + + Pricing + +

+

+ Choose the plan that works best for you +

+
+ ); +} + +function PricingCards() { + return ( +
+ {pricingTiers.map((tier) => ( +
+
+

{tier.name}

+
+ {tier.price} + {tier.period && ( + /{tier.period} + )} +
+

{tier.description}

+
+ +
    + {tier.features.map((feature) => ( +
  • + + {feature} +
  • + ))} +
+ +
+ {tier.isGitHub ? ( + + + {tier.buttonText} + + ) : ( + + {tier.buttonText} + + )} +
+
+ ))} +
+ ); +} + +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 ( +
+

+ Frequently Asked Questions +

+
+ {faqs.map((faq) => ( +
+

{faq.question}

+

{faq.answer}

+
+ ))} +
+
+ ); +} + +function Footer() { + const currentYear = new Date().getFullYear(); + + return ( +
+
+ © 2024-{currentYear}{" "} + + Localhost Labs Ltd + +
+ +
+ ); +} + +export default function Pricing() { + return ( +
+
+ +
+ + + +
+
+
+ ); +} 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 -- cgit v1.2.3-70-g09d2