feat: pricing plan & gpu ui
This commit is contained in:
parent
6de7bf3f20
commit
931b4e144a
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="px-4 sm:px-6 lg:px-8">
|
||||
<div className="-mx-4 mt-8 sm:-mx-0">
|
||||
<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>
|
||||
);
|
||||
}
|
191
web/src/app/(app)/pricing/components/pricingPlanTable.tsx
Normal file
191
web/src/app/(app)/pricing/components/pricingPlanTable.tsx
Normal file
@ -0,0 +1,191 @@
|
||||
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>
|
||||
);
|
||||
}
|
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 { PricingPlan } from "@/app/(app)/pricing/components/pricingPlanTable";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div>
|
||||
<PricingPlan />
|
||||
<GpuPricingPlan />
|
||||
</div>
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user