forked from mrwyndham/fastpocket
feature - added one time payment processing
This commit is contained in:
parent
fb64f6bcd6
commit
28e5d2c9eb
Binary file not shown.
|
@ -391,23 +391,28 @@ func main() {
|
||||||
form = forms.NewRecordUpsert(app, record)
|
form = forms.NewRecordUpsert(app, record)
|
||||||
}
|
}
|
||||||
|
|
||||||
form.LoadData(map[string]any{
|
data := map[string]any{
|
||||||
"price_id": price.ID,
|
"price_id": price.ID,
|
||||||
"product_id": price.Product.ID,
|
"product_id": price.Product.ID,
|
||||||
"active": price.Active,
|
"active": price.Active,
|
||||||
"currency": price.Currency,
|
"currency": price.Currency,
|
||||||
"description": price.Nickname,
|
"description": price.Nickname,
|
||||||
"type": price.Type,
|
"type": price.Type,
|
||||||
"unit_amount": price.UnitAmount,
|
"unit_amount": price.UnitAmount,
|
||||||
"interval": price.Recurring.Interval,
|
"metadata": price.Metadata,
|
||||||
"interval_count": price.Recurring.IntervalCount,
|
}
|
||||||
"trial_period_days": price.Recurring.TrialPeriodDays,
|
// Check if Recurring is not nil before accessing its fields
|
||||||
"metadata": price.Metadata,
|
if price.Recurring != nil {
|
||||||
})
|
data["interval"] = price.Recurring.Interval
|
||||||
|
data["interval_count"] = price.Recurring.IntervalCount
|
||||||
|
data["trial_period_days"] = price.Recurring.TrialPeriodDays
|
||||||
|
}
|
||||||
|
|
||||||
|
form.LoadData(data)
|
||||||
|
|
||||||
// validate and submit (internally it calls app.Dao().SaveRecord(record) in a transaction)
|
// validate and submit (internally it calls app.Dao().SaveRecord(record) in a transaction)
|
||||||
if err := form.Submit(); err != nil {
|
if err := form.Submit(); err != nil {
|
||||||
return err
|
return c.JSON(http.StatusBadRequest, map[string]string{"failure": "failed to submit to pocketbase"})
|
||||||
}
|
}
|
||||||
case "customer.subscription.created", "customer.subscription.updated", "customer.subscription.deleted":
|
case "customer.subscription.created", "customer.subscription.updated", "customer.subscription.deleted":
|
||||||
var subscription stripe.Subscription
|
var subscription stripe.Subscription
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
import { Product, Price } from "@/types";
|
import { Product, Price } from "@/types";
|
||||||
import pb from "@/lib/pocketbase";
|
import pb from "@/lib/pocketbase";
|
||||||
import { getAuthCookie } from "@/app/(auth)/actions";
|
|
||||||
|
|
||||||
export async function apiPrices() {
|
export async function apiPrices() {
|
||||||
try {
|
try {
|
||||||
|
@ -30,11 +29,12 @@ export async function apiPrices() {
|
||||||
product.metadata.benefits = product?.metadata?.benefits ? JSON.parse(product.metadata.benefits as any) : [];
|
product.metadata.benefits = product?.metadata?.benefits ? JSON.parse(product.metadata.benefits as any) : [];
|
||||||
const pricesOfProduct = prices.filter(price => price.product_id === product.product_id);
|
const pricesOfProduct = prices.filter(price => price.product_id === product.product_id);
|
||||||
for (const priceOfProduct of pricesOfProduct){
|
for (const priceOfProduct of pricesOfProduct){
|
||||||
if (priceOfProduct.interval === "year"){
|
product.type = priceOfProduct.type;
|
||||||
product.yearlyPrice = priceOfProduct;
|
if (priceOfProduct.interval === "year"){
|
||||||
} else {
|
product.yearlyPrice = priceOfProduct;
|
||||||
product.monthlyPrice = priceOfProduct;
|
} else {
|
||||||
}
|
product.monthlyPrice = priceOfProduct;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,35 +1,13 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect } from "react";
|
import React from "react";
|
||||||
import PageHeader from "@/sections/PageHeader";
|
import PageHeader from "@/sections/PageHeader";
|
||||||
import { Price, Product, SourceModal } from "@/types";
|
|
||||||
import { Check } from "@styled-icons/entypo/Check";
|
|
||||||
import { ChangeEventHandler, useState } from "react";
|
|
||||||
import { apiPrices } from "./actions";
|
|
||||||
import Newsletter from "@/sections/Newsletter/Newsletter";
|
import Newsletter from "@/sections/Newsletter/Newsletter";
|
||||||
import { createCheckoutSession, isAuthenticated } from "@/app/(auth)/actions";
|
|
||||||
import { toast } from "react-toastify";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import Background from "@/components/Utilities/Background";
|
import Background from "@/components/Utilities/Background";
|
||||||
import Footer from "@/components/Footer";
|
import Footer from "@/components/Footer";
|
||||||
|
import Payment from "@/sections/Payment";
|
||||||
|
|
||||||
export default function PricingPage() {
|
export default function PricingPage() {
|
||||||
const [isAnnual, setIsAnnual] = useState(false);
|
|
||||||
const [products, setProducts] = useState<Product[]>([]);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
|
|
||||||
const handleToggle = () => {
|
|
||||||
setIsAnnual((prev) => !prev);
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
|
||||||
setIsLoading(true);
|
|
||||||
(async () => {
|
|
||||||
const resposeProducts: Product[] = await apiPrices();
|
|
||||||
setProducts(resposeProducts);
|
|
||||||
setIsLoading(false);
|
|
||||||
})();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main
|
<main
|
||||||
id="content"
|
id="content"
|
||||||
|
@ -49,177 +27,10 @@ export default function PricingPage() {
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center justify-center mt-10">
|
<Payment type="one_time" />
|
||||||
<PriceToggle isAnnual={isAnnual} onChange={handleToggle} />
|
|
||||||
</div>
|
|
||||||
<div className="max-w-6xl mx-auto mb-24 h-full">
|
|
||||||
<div
|
|
||||||
className={`w-screen lg:w-full flex gap-x-4 lg:justify-center gap-y-8 px-6 pt-12 overflow-x-scroll`}
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<>
|
|
||||||
<PriceCard loading={isLoading} />
|
|
||||||
<PriceCard loading={isLoading} />
|
|
||||||
<PriceCard loading={isLoading} />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
products.map((x, i) => (
|
|
||||||
<PriceCard key={i} product={x} isAnnual={isAnnual} />
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Newsletter />
|
<Newsletter />
|
||||||
<Footer />
|
<Footer />
|
||||||
</Background>
|
</Background>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PriceCard({
|
|
||||||
product,
|
|
||||||
isAnnual,
|
|
||||||
loading,
|
|
||||||
}: {
|
|
||||||
product?: Product;
|
|
||||||
isAnnual?: boolean;
|
|
||||||
loading?: boolean;
|
|
||||||
}) {
|
|
||||||
const router = useRouter();
|
|
||||||
const openSignUpModalOnPriceClick = (price: Price) => {
|
|
||||||
const signUpModal = document.getElementById("sign-up-modal");
|
|
||||||
if (!signUpModal) return;
|
|
||||||
signUpModal.setAttribute("price_id", price.price_id);
|
|
||||||
signUpModal.setAttribute("name", SourceModal.SignUpViaPurchase);
|
|
||||||
signUpModal.click();
|
|
||||||
};
|
|
||||||
const generateCheckoutPage = async (price: Price) => {
|
|
||||||
try {
|
|
||||||
const checkoutSessionResponse = await createCheckoutSession(
|
|
||||||
price.price_id
|
|
||||||
);
|
|
||||||
router.push(checkoutSessionResponse.url);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
toast.error(error.message, {
|
|
||||||
position: "bottom-left",
|
|
||||||
autoClose: 5000,
|
|
||||||
hideProgressBar: false,
|
|
||||||
closeOnClick: true,
|
|
||||||
pauseOnHover: true,
|
|
||||||
draggable: true,
|
|
||||||
progress: undefined,
|
|
||||||
theme: "colored",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const submitForm = async (price: Price) => {
|
|
||||||
const userIsAuthenticated = await isAuthenticated();
|
|
||||||
if (userIsAuthenticated) {
|
|
||||||
await generateCheckoutPage(price);
|
|
||||||
} else {
|
|
||||||
localStorage.setItem("price", JSON.stringify(price));
|
|
||||||
openSignUpModalOnPriceClick(price);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`relative w-64 sm:w-80 bg-base-100 rounded-lg p-6 ${
|
|
||||||
loading ? "animate-pulse h-[20rem]" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col h-full">
|
|
||||||
<div className="mb-12 relative">
|
|
||||||
{false && (
|
|
||||||
<p className="absolute top-[-10px] left-[50%] -translate-x-1/2 font-architects-daughter text-xl text-primary text-center">
|
|
||||||
Popular
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<h1 className="text-center text-3xl font-inter font-bold pt-6 text-base-content ">
|
|
||||||
{product?.name}
|
|
||||||
</h1>
|
|
||||||
<h3 className="text-center pt-4 text-base-content ">
|
|
||||||
{product?.description}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div className="pb-12">
|
|
||||||
<ul className="flex flex-col gap-y-3 mx-12">
|
|
||||||
{product?.metadata?.benefits?.map((x, i) => (
|
|
||||||
<li key={i} className="flex items-center gap-x-4 flex-nowrap">
|
|
||||||
<Check className=" self-start" color="#FF0DCA" size={24} />
|
|
||||||
|
|
||||||
<p className="w-40 text-base-content overflow-clip text-ellipsis">
|
|
||||||
{x}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col mx-auto mt-auto">
|
|
||||||
{!loading && (
|
|
||||||
<div className="flex flex-row mx-auto gap-x-2 justify-center items-center mb-2">
|
|
||||||
<h1 className="text-base-content text-4xl font-bold">
|
|
||||||
$
|
|
||||||
{isAnnual
|
|
||||||
? (product?.yearlyPrice?.unit_amount ?? 0) / 100
|
|
||||||
: (product?.monthlyPrice?.unit_amount ?? 0) / 100}
|
|
||||||
</h1>
|
|
||||||
<p className="w-16 leading-5 text-sm text-base-content ">
|
|
||||||
per user per {isAnnual ? "year" : "month"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{product && (
|
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
submitForm(
|
|
||||||
isAnnual ? product.yearlyPrice : product.monthlyPrice
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="btn btn-primary rounded-full bg-gradient-to-r from-primary to-secondary"
|
|
||||||
>
|
|
||||||
Try it!
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function PriceToggle({
|
|
||||||
isAnnual,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
isAnnual: boolean;
|
|
||||||
onChange?: ChangeEventHandler<HTMLInputElement>;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<label className="shadow-card relative inline-flex cursor-pointer select-none items-center justify-center rounded-lg bg-base-100 p-1">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="sr-only"
|
|
||||||
checked={isAnnual}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className={`flex items-center space-x-[6px] rounded py-2 px-[18px] text-sm font-medium ${
|
|
||||||
!isAnnual ? " bg-secondary" : "text-secondary-content"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Monthly Billing
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className={`flex items-center space-x-[6px] rounded py-2 px-[18px] text-sm font-medium ${
|
|
||||||
isAnnual ? " bg-secondary" : "text-secondary-content"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Yearly Billing
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
@ -174,7 +174,7 @@ export async function logout() {
|
||||||
redirect('/');
|
redirect('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createCheckoutSession(price_id: string) {
|
export async function createCheckoutSession(price_id: string, type: string) {
|
||||||
const pocketbaseUrl = process.env.NEXT_PUBLIC_POCKETBASE_URL_STRING;
|
const pocketbaseUrl = process.env.NEXT_PUBLIC_POCKETBASE_URL_STRING;
|
||||||
if (!pocketbaseUrl) {
|
if (!pocketbaseUrl) {
|
||||||
throw Error('Connection Timeout');
|
throw Error('Connection Timeout');
|
||||||
|
@ -200,7 +200,7 @@ export async function createCheckoutSession(price_id: string) {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
price: {
|
price: {
|
||||||
id: price_id,
|
id: price_id,
|
||||||
type: "recurring"
|
type: type
|
||||||
},
|
},
|
||||||
quantity: 1
|
quantity: 1
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -20,10 +20,11 @@ function ModalSignUp() {
|
||||||
resolver: yupResolver(signUpValidationSchema),
|
resolver: yupResolver(signUpValidationSchema),
|
||||||
});
|
});
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const generateCheckoutPage = async (price: Price) => {
|
const generateCheckoutPage = async (price: Price, type: string) => {
|
||||||
try {
|
try {
|
||||||
const checkoutSessionResponse = await createCheckoutSession(
|
const checkoutSessionResponse = await createCheckoutSession(
|
||||||
price.price_id
|
price.price_id,
|
||||||
|
type
|
||||||
);
|
);
|
||||||
console.log(checkoutSessionResponse);
|
console.log(checkoutSessionResponse);
|
||||||
router.push(checkoutSessionResponse.url);
|
router.push(checkoutSessionResponse.url);
|
||||||
|
@ -66,8 +67,11 @@ function ModalSignUp() {
|
||||||
) {
|
) {
|
||||||
reset();
|
reset();
|
||||||
document.getElementById("sign-up-modal")?.click();
|
document.getElementById("sign-up-modal")?.click();
|
||||||
|
const type = document
|
||||||
|
.getElementById("sign-up-modal")
|
||||||
|
?.getAttribute("type");
|
||||||
const price = localStorage.getItem("price");
|
const price = localStorage.getItem("price");
|
||||||
price && generateCheckoutPage(JSON.parse(price));
|
price && generateCheckoutPage(JSON.parse(price), type ?? "");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("heyaa");
|
console.log("heyaa");
|
||||||
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
import { Price, Product, SourceModal } from "@/types";
|
||||||
|
import { Check } from "@styled-icons/entypo/Check";
|
||||||
|
import { createCheckoutSession, isAuthenticated } from "@/app/(auth)/actions";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
export default function PriceCard({
|
||||||
|
product,
|
||||||
|
isAnnual,
|
||||||
|
loading,
|
||||||
|
}: {
|
||||||
|
product?: Product;
|
||||||
|
isAnnual?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
|
}) {
|
||||||
|
const router = useRouter();
|
||||||
|
const openSignUpModalOnPriceClick = (price: Price, type: string) => {
|
||||||
|
const signUpModal = document.getElementById("sign-up-modal");
|
||||||
|
if (!signUpModal) return;
|
||||||
|
signUpModal.setAttribute("price_id", price.price_id);
|
||||||
|
signUpModal.setAttribute("type", type);
|
||||||
|
signUpModal.setAttribute("name", SourceModal.SignUpViaPurchase);
|
||||||
|
signUpModal.click();
|
||||||
|
};
|
||||||
|
const generateCheckoutPage = async (price: Price, type: string) => {
|
||||||
|
try {
|
||||||
|
const checkoutSessionResponse = await createCheckoutSession(
|
||||||
|
price.price_id,
|
||||||
|
type
|
||||||
|
);
|
||||||
|
router.push(checkoutSessionResponse.url);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
toast.error(error.message, {
|
||||||
|
position: "bottom-left",
|
||||||
|
autoClose: 5000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
progress: undefined,
|
||||||
|
theme: "colored",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const submitForm = async (product: Product) => {
|
||||||
|
const userIsAuthenticated = await isAuthenticated();
|
||||||
|
const price = isAnnual ? product.yearlyPrice : product.monthlyPrice;
|
||||||
|
if (userIsAuthenticated) {
|
||||||
|
await generateCheckoutPage(price, product.type);
|
||||||
|
} else {
|
||||||
|
localStorage.setItem("price", JSON.stringify(price));
|
||||||
|
openSignUpModalOnPriceClick(price, product.type);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`relative w-64 sm:w-80 bg-base-100 rounded-lg p-6 ${
|
||||||
|
loading ? "animate-pulse h-[20rem]" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div className="mb-12 relative">
|
||||||
|
{false && (
|
||||||
|
<p className="absolute top-[-10px] left-[50%] -translate-x-1/2 font-architects-daughter text-xl text-primary text-center">
|
||||||
|
Popular
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<h1 className="text-center text-3xl font-inter font-bold pt-6 text-base-content ">
|
||||||
|
{product?.name}
|
||||||
|
</h1>
|
||||||
|
<h3 className="text-center pt-4 text-base-content ">
|
||||||
|
{product?.description}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="pb-12">
|
||||||
|
<ul className="flex flex-col gap-y-3 mx-12">
|
||||||
|
{product?.metadata?.benefits?.map((x, i) => (
|
||||||
|
<li key={i} className="flex items-center gap-x-4 flex-nowrap">
|
||||||
|
<Check className=" self-start" color="#FF0DCA" size={24} />
|
||||||
|
|
||||||
|
<p className="w-40 text-base-content overflow-clip text-ellipsis">
|
||||||
|
{x}
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col mx-auto mt-auto">
|
||||||
|
{!loading && (
|
||||||
|
<div className="flex flex-row mx-auto gap-x-2 justify-center items-center mb-2">
|
||||||
|
<h1 className="text-base-content text-4xl font-bold">
|
||||||
|
$
|
||||||
|
{isAnnual
|
||||||
|
? (product?.yearlyPrice?.unit_amount ?? 0) / 100
|
||||||
|
: (product?.monthlyPrice?.unit_amount ?? 0) / 100}
|
||||||
|
</h1>
|
||||||
|
<p className="w-16 leading-5 text-sm text-base-content ">
|
||||||
|
per user per {isAnnual ? "year" : "month"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{product && (
|
||||||
|
<button
|
||||||
|
onClick={() => submitForm(product)}
|
||||||
|
className="btn btn-primary rounded-full bg-gradient-to-r from-primary to-secondary"
|
||||||
|
>
|
||||||
|
Try it!
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { ChangeEventHandler } from "react";
|
||||||
|
|
||||||
|
export default function PriceToggle({
|
||||||
|
isAnnual,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
isAnnual: boolean;
|
||||||
|
onChange?: ChangeEventHandler<HTMLInputElement>;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<label className="shadow-card relative inline-flex cursor-pointer select-none items-center justify-center rounded-lg bg-base-100 p-1">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="sr-only"
|
||||||
|
checked={isAnnual}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={`flex items-center space-x-[6px] rounded py-2 px-[18px] text-sm font-medium ${
|
||||||
|
!isAnnual ? " bg-secondary" : "text-secondary-content"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Monthly Billing
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`flex items-center space-x-[6px] rounded py-2 px-[18px] text-sm font-medium ${
|
||||||
|
isAnnual ? " bg-secondary" : "text-secondary-content"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Yearly Billing
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { Product } from "@/types";
|
||||||
|
import PriceCard from "@/components/PriceCard";
|
||||||
|
import { useState } from "react";
|
||||||
|
import PriceToggle from "@/components/PriceToggle";
|
||||||
|
import { apiPrices } from "../app/(admin)/pricing/actions";
|
||||||
|
|
||||||
|
const Payment = ({
|
||||||
|
type = "one_time",
|
||||||
|
}: {
|
||||||
|
type?: "one_time" | "reoccuring";
|
||||||
|
}) => {
|
||||||
|
const [isAnnual, setIsAnnual] = useState(false);
|
||||||
|
const [products, setProducts] = useState<Product[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const handleToggle = () => {
|
||||||
|
setIsAnnual((prev) => !prev);
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoading(true);
|
||||||
|
(async () => {
|
||||||
|
const resposeProducts: Product[] = await apiPrices();
|
||||||
|
setProducts(resposeProducts);
|
||||||
|
setIsLoading(false);
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-center mt-10">
|
||||||
|
<PriceToggle isAnnual={isAnnual} onChange={handleToggle} />
|
||||||
|
</div>
|
||||||
|
<div className="max-w-6xl mx-auto mb-24 h-full">
|
||||||
|
<div
|
||||||
|
className={`w-screen lg:w-full flex gap-x-4 lg:justify-center gap-y-8 px-6 pt-12 overflow-x-scroll`}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<PriceCard loading={isLoading} />
|
||||||
|
<PriceCard loading={isLoading} />
|
||||||
|
<PriceCard loading={isLoading} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
products
|
||||||
|
.filter((x) =>
|
||||||
|
type == "one_time" ? x.type == "one_time" : x.type != "one_time"
|
||||||
|
)
|
||||||
|
.map((x, i) => (
|
||||||
|
<PriceCard key={i} product={x} isAnnual={isAnnual} />
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Payment;
|
|
@ -29,6 +29,7 @@ export type Product = {
|
||||||
product_order: number;
|
product_order: number;
|
||||||
yearlyPrice: Price;
|
yearlyPrice: Price;
|
||||||
monthlyPrice: Price;
|
monthlyPrice: Price;
|
||||||
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Price = {
|
export type Price = {
|
||||||
|
|
Loading…
Reference in New Issue