Merge branch 'pricing-plan'
# Conflicts: # web/bun.lockb # web/drizzle/meta/_journal.json # web/src/db/schema.ts
This commit is contained in:
		
						commit
						14c3ca6bf5
					
				@ -248,4 +248,4 @@
 | 
				
			|||||||
      "breakpoints": true
 | 
					      "breakpoints": true
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  ]
 | 
					  ]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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",
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,13 @@
 | 
				
			|||||||
import { parseDataSafe } from "../../../../lib/parseDataSafe";
 | 
					import { parseDataSafe } from "../../../../lib/parseDataSafe";
 | 
				
			||||||
import { db } from "@/db/db";
 | 
					import { db } from "@/db/db";
 | 
				
			||||||
import { workflowRunOutputs, workflowRunsTable } from "@/db/schema";
 | 
					import {
 | 
				
			||||||
 | 
					  userUsageTable,
 | 
				
			||||||
 | 
					  workflowRunOutputs,
 | 
				
			||||||
 | 
					  workflowRunsTable,
 | 
				
			||||||
 | 
					  workflowTable,
 | 
				
			||||||
 | 
					} from "@/db/schema";
 | 
				
			||||||
 | 
					import { getDuration } from "@/lib/getRelativeTime";
 | 
				
			||||||
 | 
					import { getSubscription, setUsage } from "@/server/linkToPricing";
 | 
				
			||||||
import { eq } from "drizzle-orm";
 | 
					import { eq } from "drizzle-orm";
 | 
				
			||||||
import { NextResponse } from "next/server";
 | 
					import { NextResponse } from "next/server";
 | 
				
			||||||
import { z } from "zod";
 | 
					import { z } from "zod";
 | 
				
			||||||
@ -27,7 +34,6 @@ export async function POST(request: Request) {
 | 
				
			|||||||
      data: output_data,
 | 
					      data: output_data,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  } else if (status) {
 | 
					  } else if (status) {
 | 
				
			||||||
    // console.log("status", status);
 | 
					 | 
				
			||||||
    const workflow_run = await db
 | 
					    const workflow_run = await db
 | 
				
			||||||
      .update(workflowRunsTable)
 | 
					      .update(workflowRunsTable)
 | 
				
			||||||
      .set({
 | 
					      .set({
 | 
				
			||||||
@ -35,8 +41,15 @@ export async function POST(request: Request) {
 | 
				
			|||||||
        ended_at:
 | 
					        ended_at:
 | 
				
			||||||
          status === "success" || status === "failed" ? new Date() : null,
 | 
					          status === "success" || status === "failed" ? new Date() : null,
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
      .where(eq(workflowRunsTable.id, run_id))
 | 
					      .where(eq(workflowRunsTable.id, run_id));
 | 
				
			||||||
      .returning();
 | 
					
 | 
				
			||||||
 | 
					    // get data from workflowRunsTable
 | 
				
			||||||
 | 
					    const userUsageTime = await importUserUsageData(run_id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (userUsageTime) {
 | 
				
			||||||
 | 
					      // get the usage_time from userUsage
 | 
				
			||||||
 | 
					      await addSubscriptionUnit(userUsageTime);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // const workflow_version = await db.query.workflowVersionTable.findFirst({
 | 
					  // const workflow_version = await db.query.workflowVersionTable.findFirst({
 | 
				
			||||||
@ -54,3 +67,47 @@ export async function POST(request: Request) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function addSubscriptionUnit(userUsageTime: number) {
 | 
				
			||||||
 | 
					  const subscription = await getSubscription();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // round up userUsageTime to the nearest integer
 | 
				
			||||||
 | 
					  const roundedUsageTime = Math.ceil(userUsageTime);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (subscription) {
 | 
				
			||||||
 | 
					    const usage = await setUsage(
 | 
				
			||||||
 | 
					      subscription.data[0].attributes.first_subscription_item.id,
 | 
				
			||||||
 | 
					      roundedUsageTime
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function importUserUsageData(run_id: string) {
 | 
				
			||||||
 | 
					  const workflowRuns = await db.query.workflowRunsTable.findFirst({
 | 
				
			||||||
 | 
					    where: eq(workflowRunsTable.id, run_id),
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!workflowRuns?.workflow_id) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // find if workflowTable id column contains workflowRunsTable workflow_id
 | 
				
			||||||
 | 
					  const workflow = await db.query.workflowTable.findFirst({
 | 
				
			||||||
 | 
					    where: eq(workflowTable.id, workflowRuns.workflow_id),
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (workflowRuns?.ended_at === null || workflow == null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const usageTime = parseFloat(
 | 
				
			||||||
 | 
					    getDuration((workflowRuns?.ended_at - workflowRuns?.started_at) / 1000)
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // add data to userUsageTable
 | 
				
			||||||
 | 
					  const user_usage = await db.insert(userUsageTable).values({
 | 
				
			||||||
 | 
					    user_id: workflow.user_id,
 | 
				
			||||||
 | 
					    created_at: workflowRuns.ended_at,
 | 
				
			||||||
 | 
					    org_id: workflow.org_id,
 | 
				
			||||||
 | 
					    ended_at: workflowRuns.ended_at,
 | 
				
			||||||
 | 
					    usage_time: usageTime,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return usageTime;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										81
									
								
								web/src/app/(app)/pricing/components/gpuPricingTable.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								web/src/app/(app)/pricing/components/gpuPricingTable.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,81 @@
 | 
				
			|||||||
 | 
					const people = [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: "Nvidia T4 GPU",
 | 
				
			||||||
 | 
					    gpu: "1x",
 | 
				
			||||||
 | 
					    ram: "16GB",
 | 
				
			||||||
 | 
					    price: "$0.000225/sec",
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: "Nvidia A40 GPU",
 | 
				
			||||||
 | 
					    gpu: "1x",
 | 
				
			||||||
 | 
					    ram: "48GB",
 | 
				
			||||||
 | 
					    price: "$0.000575/sec",
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function GpuPricingPlan() {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="flex justify-center w-full py-8">
 | 
				
			||||||
 | 
					      <div className="w-full max-w-4xl">
 | 
				
			||||||
 | 
					        <table className="min-w-full divide-y divide-gray-300">
 | 
				
			||||||
 | 
					          <thead>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					              <th
 | 
				
			||||||
 | 
					                scope="col"
 | 
				
			||||||
 | 
					                className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                GPU
 | 
				
			||||||
 | 
					              </th>
 | 
				
			||||||
 | 
					              <th
 | 
				
			||||||
 | 
					                scope="col"
 | 
				
			||||||
 | 
					                className="hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                No.
 | 
				
			||||||
 | 
					              </th>
 | 
				
			||||||
 | 
					              <th
 | 
				
			||||||
 | 
					                scope="col"
 | 
				
			||||||
 | 
					                className="hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                RAM
 | 
				
			||||||
 | 
					              </th>
 | 
				
			||||||
 | 
					              <th
 | 
				
			||||||
 | 
					                scope="col"
 | 
				
			||||||
 | 
					                className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                Price
 | 
				
			||||||
 | 
					              </th>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					          </thead>
 | 
				
			||||||
 | 
					          <tbody className="divide-y divide-gray-200 bg-white">
 | 
				
			||||||
 | 
					            {people.map((person) => (
 | 
				
			||||||
 | 
					              <tr key={person.ram} className="even:bg-gray-50">
 | 
				
			||||||
 | 
					                <td className="w-full max-w-0 py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:w-auto sm:max-w-none sm:pl-6">
 | 
				
			||||||
 | 
					                  {person.name}
 | 
				
			||||||
 | 
					                  <dl className="font-normal lg:hidden">
 | 
				
			||||||
 | 
					                    <dt className="sr-only">No.</dt>
 | 
				
			||||||
 | 
					                    <dd className="mt-1 truncate text-gray-700">
 | 
				
			||||||
 | 
					                      {person.gpu}
 | 
				
			||||||
 | 
					                    </dd>
 | 
				
			||||||
 | 
					                    <dt className="sr-only sm:hidden">RAM</dt>
 | 
				
			||||||
 | 
					                    <dd className="mt-1 truncate text-gray-500 sm:hidden">
 | 
				
			||||||
 | 
					                      {person.ram}
 | 
				
			||||||
 | 
					                    </dd>
 | 
				
			||||||
 | 
					                  </dl>
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td className="hidden px-3 py-4 text-sm text-gray-500 lg:table-cell">
 | 
				
			||||||
 | 
					                  {person.gpu}
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td className="hidden px-3 py-4 text-sm text-gray-500 sm:table-cell">
 | 
				
			||||||
 | 
					                  {person.ram}
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td className="px-3 py-4 text-sm text-gray-500">
 | 
				
			||||||
 | 
					                  {person.price}
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					              </tr>
 | 
				
			||||||
 | 
					            ))}
 | 
				
			||||||
 | 
					          </tbody>
 | 
				
			||||||
 | 
					        </table>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										157
									
								
								web/src/app/(app)/pricing/components/pricePlanList.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								web/src/app/(app)/pricing/components/pricePlanList.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,157 @@
 | 
				
			|||||||
 | 
					import { checkMarkIcon, crossMarkIcon } 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;
 | 
				
			||||||
 | 
					  priority?: TierPriority;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum TierPriority {
 | 
				
			||||||
 | 
					  Free = "free",
 | 
				
			||||||
 | 
					  Pro = "pro",
 | 
				
			||||||
 | 
					  Enterprise = "enterprise",
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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] == "Usage-based"
 | 
				
			||||||
 | 
					              ? "$20.00"
 | 
				
			||||||
 | 
					              : item.attributes.price_formatted.split("/")[0],
 | 
				
			||||||
 | 
					          description: description,
 | 
				
			||||||
 | 
					          features: features,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          // if name contains pro, it's featured
 | 
				
			||||||
 | 
					          featured: item.attributes.name.toLowerCase().includes("pro"),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          // give priority if name contain in enum
 | 
				
			||||||
 | 
					          priority: Object.values(TierPriority).find((priority) =>
 | 
				
			||||||
 | 
					            item.attributes.name.toLowerCase().includes(priority)
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // sort newProductTiers by priority
 | 
				
			||||||
 | 
					      newProductTiers.sort((a, b) => {
 | 
				
			||||||
 | 
					        if (!a.priority) return 1;
 | 
				
			||||||
 | 
					        if (!b.priority) return -1;
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          Object.values(TierPriority).indexOf(a.priority) -
 | 
				
			||||||
 | 
					          Object.values(TierPriority).indexOf(b.priority)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      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 xl:max-w-6xl xl:grid-cols-3">
 | 
				
			||||||
 | 
					        {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">
 | 
				
			||||||
 | 
					                      {feature.includes("[x]") ? crossMarkIcon : checkMarkIcon}
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    {feature.replace("[x]", "")}
 | 
				
			||||||
 | 
					                  </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>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										37
									
								
								web/src/app/(app)/pricing/const/Icon.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								web/src/app/(app)/pricing/const/Icon.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					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>
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const crossMarkIcon = (
 | 
				
			||||||
 | 
					  <svg
 | 
				
			||||||
 | 
					    xmlns="http://www.w3.org/2000/svg"
 | 
				
			||||||
 | 
					    x="0px"
 | 
				
			||||||
 | 
					    y="0px"
 | 
				
			||||||
 | 
					    width="20"
 | 
				
			||||||
 | 
					    height="20"
 | 
				
			||||||
 | 
					    viewBox="0 0 48 48"
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <path
 | 
				
			||||||
 | 
					      fill="#F44336"
 | 
				
			||||||
 | 
					      d="M21.5 4.5H26.501V43.5H21.5z"
 | 
				
			||||||
 | 
					      transform="rotate(45.001 24 24)"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					    <path
 | 
				
			||||||
 | 
					      fill="#F44336"
 | 
				
			||||||
 | 
					      d="M21.5 4.5H26.5V43.501H21.5z"
 | 
				
			||||||
 | 
					      transform="rotate(135.008 24 24)"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  </svg>
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
							
								
								
									
										9
									
								
								web/src/app/(app)/pricing/loading.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								web/src/app/(app)/pricing/loading.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { LoadingPageWrapper } from "@/components/LoadingWrapper";
 | 
				
			||||||
 | 
					import { usePathname } from "next/navigation";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function Loading() {
 | 
				
			||||||
 | 
					  const pathName = usePathname();
 | 
				
			||||||
 | 
					  return <LoadingPageWrapper className="h-full" tag={pathName.toLowerCase()} />;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										13
									
								
								web/src/app/(app)/pricing/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								web/src/app/(app)/pricing/page.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { GpuPricingPlan } from "@/app/(app)/pricing/components/gpuPricingTable";
 | 
				
			||||||
 | 
					import PricingList from "@/app/(app)/pricing/components/pricePlanList";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function Home() {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div>
 | 
				
			||||||
 | 
					      <PricingList />
 | 
				
			||||||
 | 
					      <GpuPricingPlan />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -22,6 +22,7 @@ import {
 | 
				
			|||||||
} from "@clerk/nextjs";
 | 
					} from "@clerk/nextjs";
 | 
				
			||||||
import { Github, Menu } from "lucide-react";
 | 
					import { Github, Menu } from "lucide-react";
 | 
				
			||||||
import meta from "next-gen/config";
 | 
					import meta from "next-gen/config";
 | 
				
			||||||
 | 
					import { useFeatureFlagEnabled } from "posthog-js/react";
 | 
				
			||||||
import { useEffect, useState } from "react";
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
import { useMediaQuery } from "usehooks-ts";
 | 
					import { useMediaQuery } from "usehooks-ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -29,9 +30,13 @@ export function Navbar() {
 | 
				
			|||||||
  const { organization } = useOrganization();
 | 
					  const { organization } = useOrganization();
 | 
				
			||||||
  const _isDesktop = useMediaQuery("(min-width: 1024px)");
 | 
					  const _isDesktop = useMediaQuery("(min-width: 1024px)");
 | 
				
			||||||
  const [isDesktop, setIsDesktop] = useState(true);
 | 
					  const [isDesktop, setIsDesktop] = useState(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const pricingPlanFlagEnable = useFeatureFlagEnabled("pricing-plan");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    setIsDesktop(_isDesktop);
 | 
					    setIsDesktop(_isDesktop);
 | 
				
			||||||
  }, [_isDesktop]);
 | 
					  }, [_isDesktop]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
      <div className="flex flex-row items-center gap-4">
 | 
					      <div className="flex flex-row items-center gap-4">
 | 
				
			||||||
@ -85,6 +90,15 @@ export function Navbar() {
 | 
				
			|||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div className="flex flex-row items-center gap-2">
 | 
					      <div className="flex flex-row items-center gap-2">
 | 
				
			||||||
        {isDesktop && <NavbarMenu />}
 | 
					        {isDesktop && <NavbarMenu />}
 | 
				
			||||||
 | 
					        {pricingPlanFlagEnable && (
 | 
				
			||||||
 | 
					          <Button
 | 
				
			||||||
 | 
					            asChild
 | 
				
			||||||
 | 
					            variant="link"
 | 
				
			||||||
 | 
					            className="rounded-full aspect-square p-2 mr-4"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <a href="/pricing">Pricing</a>
 | 
				
			||||||
 | 
					          </Button>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
        <Button
 | 
					        <Button
 | 
				
			||||||
          asChild
 | 
					          asChild
 | 
				
			||||||
          variant="link"
 | 
					          variant="link"
 | 
				
			||||||
@ -98,7 +112,11 @@ export function Navbar() {
 | 
				
			|||||||
          variant="outline"
 | 
					          variant="outline"
 | 
				
			||||||
          className="rounded-full aspect-square p-2"
 | 
					          className="rounded-full aspect-square p-2"
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <a target="_blank" href="https://github.com/BennyKok/comfyui-deploy" rel="noreferrer">
 | 
					          <a
 | 
				
			||||||
 | 
					            target="_blank"
 | 
				
			||||||
 | 
					            href="https://github.com/BennyKok/comfyui-deploy"
 | 
				
			||||||
 | 
					            rel="noreferrer"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
            <Github />
 | 
					            <Github />
 | 
				
			||||||
          </a>
 | 
					          </a>
 | 
				
			||||||
        </Button>
 | 
					        </Button>
 | 
				
			||||||
 | 
				
			|||||||
@ -8,6 +8,7 @@ import {
 | 
				
			|||||||
  text,
 | 
					  text,
 | 
				
			||||||
  timestamp,
 | 
					  timestamp,
 | 
				
			||||||
  uuid,
 | 
					  uuid,
 | 
				
			||||||
 | 
					  real,
 | 
				
			||||||
} from "drizzle-orm/pg-core";
 | 
					} from "drizzle-orm/pg-core";
 | 
				
			||||||
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
 | 
					import { createInsertSchema, createSelectSchema } from "drizzle-zod";
 | 
				
			||||||
import { z } from "zod";
 | 
					import { z } from "zod";
 | 
				
			||||||
@ -334,6 +335,19 @@ export const apiKeyTable = dbSchema.table("api_keys", {
 | 
				
			|||||||
  updated_at: timestamp("updated_at").defaultNow().notNull(),
 | 
					  updated_at: timestamp("updated_at").defaultNow().notNull(),
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const userUsageTable = dbSchema.table("user_usage", {
 | 
				
			||||||
 | 
					  id: uuid("id").primaryKey().defaultRandom().notNull(),
 | 
				
			||||||
 | 
					  org_id: text("org_id"),
 | 
				
			||||||
 | 
					  user_id: text("user_id")
 | 
				
			||||||
 | 
					    .references(() => usersTable.id, {
 | 
				
			||||||
 | 
					      onDelete: "cascade",
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .notNull(),
 | 
				
			||||||
 | 
					  usage_time: real("usage_time").default(0).notNull(),
 | 
				
			||||||
 | 
					  created_at: timestamp("created_at").defaultNow().notNull(),
 | 
				
			||||||
 | 
					  ended_at: timestamp("ended_at").defaultNow().notNull(),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const authRequestsTable = dbSchema.table("auth_requests", {
 | 
					export const authRequestsTable = dbSchema.table("auth_requests", {
 | 
				
			||||||
  request_id: text("request_id").primaryKey().notNull(),
 | 
					  request_id: text("request_id").primaryKey().notNull(),
 | 
				
			||||||
  user_id: text("user_id"),
 | 
					  user_id: text("user_id"),
 | 
				
			||||||
@ -349,3 +363,4 @@ export type WorkflowType = InferSelectModel<typeof workflowTable>;
 | 
				
			|||||||
export type MachineType = InferSelectModel<typeof machinesTable>;
 | 
					export type MachineType = InferSelectModel<typeof machinesTable>;
 | 
				
			||||||
export type WorkflowVersionType = InferSelectModel<typeof workflowVersionTable>;
 | 
					export type WorkflowVersionType = InferSelectModel<typeof workflowVersionTable>;
 | 
				
			||||||
export type DeploymentType = InferSelectModel<typeof deploymentsTable>;
 | 
					export type DeploymentType = InferSelectModel<typeof deploymentsTable>;
 | 
				
			||||||
 | 
					export type UserUsageType = InferSelectModel<typeof userUsageTable>;
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										45
									
								
								web/src/server/linkToPricing.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								web/src/server/linkToPricing.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					"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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function getUsage() {
 | 
				
			||||||
 | 
					  const usageRecord = await ls.getUsageRecords();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return usageRecord;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function setUsage(id: number, quantity: number) {
 | 
				
			||||||
 | 
					  const setUsage = await ls.createUsageRecord({
 | 
				
			||||||
 | 
					    subscriptionItemId: id,
 | 
				
			||||||
 | 
					    quantity: quantity,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return setUsage;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function getSubscription() {
 | 
				
			||||||
 | 
					  const subscription = await ls.getSubscriptions();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return subscription;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function getSubscriptionItem() {
 | 
				
			||||||
 | 
					  const subscriptionItem = await ls.getSubscriptionItems();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return subscriptionItem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function getUserData() {
 | 
				
			||||||
 | 
					  const user = await ls.getUser();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return user;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user