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-validator": "^0.1.11",
 | 
			
		||||
    "@hookform/resolvers": "^3.3.2",
 | 
			
		||||
    "@lemonsqueezy/lemonsqueezy.js": "^1.2.5",
 | 
			
		||||
    "@mdx-js/loader": "^3.0.0",
 | 
			
		||||
    "@mdx-js/react": "^3.0.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";
 | 
			
		||||
 | 
			
		||||
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() {
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <PricingPlan />
 | 
			
		||||
      <PricingList />
 | 
			
		||||
      <GpuPricingPlan />
 | 
			
		||||
    </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