feat: implement lemonsqueezy to pricing table
This commit is contained in:
parent
931b4e144a
commit
9cbf0760a0
@ -26,6 +26,7 @@
|
|||||||
"@hono/zod-openapi": "^0.9.5",
|
"@hono/zod-openapi": "^0.9.5",
|
||||||
"@hono/zod-validator": "^0.1.11",
|
"@hono/zod-validator": "^0.1.11",
|
||||||
"@hookform/resolvers": "^3.3.2",
|
"@hookform/resolvers": "^3.3.2",
|
||||||
|
"@lemonsqueezy/lemonsqueezy.js": "^1.2.5",
|
||||||
"@mdx-js/loader": "^3.0.0",
|
"@mdx-js/loader": "^3.0.0",
|
||||||
"@mdx-js/react": "^3.0.0",
|
"@mdx-js/react": "^3.0.0",
|
||||||
"@neondatabase/serverless": "^0.6.0",
|
"@neondatabase/serverless": "^0.6.0",
|
||||||
|
133
web/src/app/(app)/pricing/components/pricePlanList.tsx
Normal file
133
web/src/app/(app)/pricing/components/pricePlanList.tsx
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { checkMarkIcon } from "../const/Icon";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { getPricing } from "@/server/linkToPricing";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
type Tier = {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
href: string;
|
||||||
|
priceMonthly: string;
|
||||||
|
description: string;
|
||||||
|
features: string[];
|
||||||
|
featured: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PricingList() {
|
||||||
|
const [productTiers, setProductTiers] = useState<Tier[]>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const product = await getPricing();
|
||||||
|
|
||||||
|
if (!product) return;
|
||||||
|
|
||||||
|
const newProductTiers: Tier[] = product.data.map((item) => {
|
||||||
|
// Create a new DOMParser instance
|
||||||
|
const parser = new DOMParser();
|
||||||
|
// Parse the description HTML string to a new document
|
||||||
|
const doc = parser.parseFromString(
|
||||||
|
item.attributes.description,
|
||||||
|
"text/html"
|
||||||
|
);
|
||||||
|
// Extract the description and features
|
||||||
|
const description = doc.querySelector("p")?.textContent || "";
|
||||||
|
const features = Array.from(doc.querySelectorAll("ul > li")).map(
|
||||||
|
(li) => li.textContent || ""
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: item.attributes.name,
|
||||||
|
id: item.id,
|
||||||
|
href: item.attributes.buy_now_url,
|
||||||
|
priceMonthly: item.attributes.price_formatted.split("/")[0],
|
||||||
|
description: description,
|
||||||
|
features: features,
|
||||||
|
|
||||||
|
// if name contains pro, it's featured
|
||||||
|
featured: item.attributes.name.toLowerCase().includes("pro"),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(newProductTiers);
|
||||||
|
setProductTiers(newProductTiers);
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative isolate px-6 py-24 lg:px-8">
|
||||||
|
<div className="mx-auto max-w-2xl text-center lg:max-w-4xl">
|
||||||
|
<h2 className="text-base font-semibold leading-7 text-indigo-600">
|
||||||
|
Pricing
|
||||||
|
</h2>
|
||||||
|
<p className="mt-2 text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl">
|
||||||
|
The right price for you, whoever you are
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p className="mx-auto mt-6 max-w-2xl text-center text-lg leading-8 text-gray-600">
|
||||||
|
Qui iusto aut est earum eos quae. Eligendi est at nam aliquid ad quo
|
||||||
|
reprehenderit in aliquid fugiat dolorum voluptatibus.
|
||||||
|
</p>
|
||||||
|
<div className="mx-auto mt-16 grid max-w-lg grid-cols-1 items-center gap-y-6 sm:mt-20 sm:gap-y-0 lg:max-w-4xl lg:grid-cols-2">
|
||||||
|
{productTiers &&
|
||||||
|
productTiers.map((tier, tierIdx) => (
|
||||||
|
<div
|
||||||
|
key={tier.id}
|
||||||
|
className={cn(
|
||||||
|
tier.featured
|
||||||
|
? "relative bg-white shadow-2xl"
|
||||||
|
: "bg-white/60 sm:mx-8 lg:mx-0",
|
||||||
|
tier.featured
|
||||||
|
? ""
|
||||||
|
: tierIdx === 0
|
||||||
|
? "rounded-t-3xl sm:rounded-b-none lg:rounded-tr-none lg:rounded-bl-3xl"
|
||||||
|
: "sm:rounded-t-none lg:rounded-tr-3xl lg:rounded-bl-none",
|
||||||
|
"rounded-3xl p-8 ring-1 ring-gray-900/10 sm:p-10"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
id={tier.id}
|
||||||
|
className="text-base font-semibold leading-7 text-indigo-600"
|
||||||
|
>
|
||||||
|
{tier.name}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-4 flex items-baseline gap-x-2">
|
||||||
|
<span className="text-5xl font-bold tracking-tight text-gray-900">
|
||||||
|
{tier.priceMonthly}
|
||||||
|
</span>
|
||||||
|
<span className="text-base text-gray-500">/month</span>
|
||||||
|
</p>
|
||||||
|
<p className="mt-6 text-base leading-7 text-gray-600">
|
||||||
|
{tier.description}
|
||||||
|
</p>
|
||||||
|
<ul
|
||||||
|
role="list"
|
||||||
|
className="mt-8 space-y-3 text-sm leading-6 text-gray-600 sm:mt-10"
|
||||||
|
>
|
||||||
|
{tier.features.map((feature) => (
|
||||||
|
<li key={feature} className="flex gap-x-3">
|
||||||
|
<div className="flex justify-center items-center">
|
||||||
|
{checkMarkIcon}
|
||||||
|
</div>
|
||||||
|
{feature}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<a
|
||||||
|
href={tier.href}
|
||||||
|
aria-describedby={tier.id}
|
||||||
|
className={cn(
|
||||||
|
tier.featured
|
||||||
|
? "bg-indigo-600 text-white shadow hover:bg-indigo-500"
|
||||||
|
: "text-indigo-600 ring-1 ring-inset ring-indigo-200 hover:ring-indigo-300",
|
||||||
|
"mt-8 block rounded-md py-2.5 px-3.5 text-center text-sm font-semibold focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 sm:mt-10"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Get started today
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,191 +0,0 @@
|
|||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { RadioGroup } from "@headlessui/react";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
// Define a type for the frequency options
|
|
||||||
type FrequencyOption = {
|
|
||||||
value: keyof TierPrice; // Use 'keyof TierPrice' instead of string
|
|
||||||
label: string;
|
|
||||||
priceSuffix: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Define a type for the tier prices
|
|
||||||
type TierPrice = {
|
|
||||||
monthly: string;
|
|
||||||
annually: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Define a type for the tier options
|
|
||||||
type TierOption = {
|
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
href: string;
|
|
||||||
price: TierPrice;
|
|
||||||
description: string;
|
|
||||||
features: string[];
|
|
||||||
mostPopular: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Define an interface for the pricing structure
|
|
||||||
interface Pricing {
|
|
||||||
frequencies: FrequencyOption[];
|
|
||||||
tiers: TierOption[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkMarkIcon = (
|
|
||||||
<svg
|
|
||||||
className="h-5 w-5 flex-shrink-0 text-green-500"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
const pricing: Pricing = {
|
|
||||||
frequencies: [
|
|
||||||
{ value: "monthly", label: "Monthly", priceSuffix: "/month" },
|
|
||||||
{ value: "annually", label: "Annually", priceSuffix: "/year" },
|
|
||||||
],
|
|
||||||
tiers: [
|
|
||||||
{
|
|
||||||
name: "Hobby",
|
|
||||||
id: "tier-hobby",
|
|
||||||
href: "#",
|
|
||||||
price: { monthly: "$15", annually: "$144" },
|
|
||||||
description: "The essentials to provide your best work for clients.",
|
|
||||||
features: ["5 products", "Up to 1,000 subscribers", "Basic analytics"],
|
|
||||||
mostPopular: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Startup",
|
|
||||||
id: "tier-startup",
|
|
||||||
href: "#",
|
|
||||||
price: { monthly: "$60", annually: "$576" },
|
|
||||||
description: "A plan that scales with your rapidly growing business.",
|
|
||||||
features: [
|
|
||||||
"25 products",
|
|
||||||
"Up to 10,000 subscribers",
|
|
||||||
"Advanced analytics",
|
|
||||||
"24-hour support response time",
|
|
||||||
"Marketing automations",
|
|
||||||
],
|
|
||||||
mostPopular: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export function PricingPlan() {
|
|
||||||
const [frequency, setFrequency] = useState<FrequencyOption>(
|
|
||||||
pricing.frequencies[0]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="mx-auto mt-16 max-w-7xl px-6 lg:px-8">
|
|
||||||
<div className="mx-auto max-w-4xl text-center">
|
|
||||||
<h1 className="text-base font-semibold leading-7 text-indigo-600">
|
|
||||||
Pricing
|
|
||||||
</h1>
|
|
||||||
<p className="mt-2 text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl">
|
|
||||||
Pricing plans for teams of all sizes
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<p className="mx-auto mt-6 max-w-2xl text-center text-lg leading-8 text-gray-600">
|
|
||||||
Choose an affordable plan that’s packed with the best features for
|
|
||||||
engaging your audience, creating customer loyalty, and driving sales.
|
|
||||||
</p>
|
|
||||||
<div className="mt-16 flex justify-center">
|
|
||||||
<RadioGroup
|
|
||||||
value={frequency}
|
|
||||||
onChange={setFrequency}
|
|
||||||
className="grid grid-cols-2 gap-x-1 rounded-full p-1 text-center text-xs font-semibold leading-5 ring-1 ring-inset ring-gray-200"
|
|
||||||
>
|
|
||||||
<RadioGroup.Label className="sr-only">
|
|
||||||
Payment frequency
|
|
||||||
</RadioGroup.Label>
|
|
||||||
{pricing.frequencies.map((option) => (
|
|
||||||
<RadioGroup.Option
|
|
||||||
key={option.value}
|
|
||||||
value={option}
|
|
||||||
className={({ checked }) => {
|
|
||||||
return cn(
|
|
||||||
checked ? "bg-indigo-600 text-white" : "text-gray-500",
|
|
||||||
"cursor-pointer rounded-full px-2.5 py-1"
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>{option.label}</span>
|
|
||||||
</RadioGroup.Option>
|
|
||||||
))}
|
|
||||||
</RadioGroup>
|
|
||||||
</div>
|
|
||||||
<div className="isolate mx-auto mt-10 grid max-w-md grid-cols-1 gap-8 md:max-w-3xl md:grid-cols-2">
|
|
||||||
{pricing.tiers.map((tier) => (
|
|
||||||
<div
|
|
||||||
key={tier.id}
|
|
||||||
className={cn(
|
|
||||||
tier.mostPopular
|
|
||||||
? "ring-2 ring-indigo-600"
|
|
||||||
: "ring-1 ring-gray-200",
|
|
||||||
"rounded-3xl p-8"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<h2
|
|
||||||
id={tier.id}
|
|
||||||
className={cn(
|
|
||||||
tier.mostPopular ? "text-indigo-600" : "text-gray-900",
|
|
||||||
"text-lg font-semibold leading-8"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{tier.name}
|
|
||||||
</h2>
|
|
||||||
<p className="mt-4 text-sm leading-6 text-gray-600">
|
|
||||||
{tier.description}
|
|
||||||
</p>
|
|
||||||
<p className="mt-6 flex items-baseline gap-x-1">
|
|
||||||
<span className="text-4xl font-bold tracking-tight text-gray-900">
|
|
||||||
{tier.price[frequency.value]}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-semibold leading-6 text-gray-600">
|
|
||||||
{frequency.priceSuffix}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
href={tier.href}
|
|
||||||
aria-describedby={tier.id}
|
|
||||||
className={cn(
|
|
||||||
tier.mostPopular
|
|
||||||
? "bg-indigo-600 text-white shadow-sm hover:bg-indigo-500"
|
|
||||||
: "text-indigo-600 ring-1 ring-inset ring-indigo-200 hover:ring-indigo-300",
|
|
||||||
"mt-6 block rounded-md py-2 px-3 text-center text-sm font-semibold leading-6 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Buy plan
|
|
||||||
</a>
|
|
||||||
<ul
|
|
||||||
role="list"
|
|
||||||
className="mt-8 space-y-3 text-sm leading-6 text-gray-600"
|
|
||||||
>
|
|
||||||
{tier.features.map((feature) => (
|
|
||||||
<li key={feature} className="flex gap-x-3">
|
|
||||||
<div className="flex justify-center items-center">
|
|
||||||
{checkMarkIcon}
|
|
||||||
</div>
|
|
||||||
{feature}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
15
web/src/app/(app)/pricing/const/Icon.tsx
Normal file
15
web/src/app/(app)/pricing/const/Icon.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export const checkMarkIcon = (
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5 flex-shrink-0 text-green-500"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
@ -1,12 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { GpuPricingPlan } from "@/app/(app)/pricing/components/gpuPricingTable";
|
import { GpuPricingPlan } from "@/app/(app)/pricing/components/gpuPricingTable";
|
||||||
import { PricingPlan } from "@/app/(app)/pricing/components/pricingPlanTable";
|
import PricingList from "@/app/(app)/pricing/components/pricePlanList";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<PricingPlan />
|
<PricingList />
|
||||||
<GpuPricingPlan />
|
<GpuPricingPlan />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
12
web/src/server/linkToPricing.ts
Normal file
12
web/src/server/linkToPricing.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { LemonSqueezy } from "@lemonsqueezy/lemonsqueezy.js";
|
||||||
|
import "server-only";
|
||||||
|
|
||||||
|
const ls = new LemonSqueezy(process.env.LEMONSQUEEZY_API_KEY || "");
|
||||||
|
|
||||||
|
export async function getPricing() {
|
||||||
|
const products = await ls.getProducts();
|
||||||
|
|
||||||
|
return products;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user