feat: lemonsqueezy1
This commit is contained in:
parent
c6c9914e84
commit
fbd899cae3
17
app/billing/page1.tsx
Normal file
17
app/billing/page1.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
// /* /app/billing/page.jsx */
|
||||
|
||||
// import Plans from "@/components/plan";
|
||||
|
||||
// export default async function Page() {
|
||||
// const plans = await getPlans();
|
||||
|
||||
// const subscription = null; // TODO
|
||||
|
||||
// return (
|
||||
// <div className="container mx-auto max-w-lg">
|
||||
// <h1 className="text-xl font-bold mb-3">Billing</h1>
|
||||
|
||||
// <Plans plans={plans} subscription={subscription} />
|
||||
// </div>
|
||||
// );
|
||||
// }
|
46
app/billing/refresh-plans/page.jsx
Normal file
46
app/billing/refresh-plans/page.jsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
/* /app/billing/refresh-plans/page.jsx */
|
||||
|
||||
import prisma from "@/lib/prisma";
|
||||
import LemonSqueezy from "@lemonsqueezy/lemonsqueezy.js";
|
||||
|
||||
const ls = new LemonSqueezy(process.env.LEMONSQUEEZY_API_KEY);
|
||||
|
||||
export const dynamic = "force-dynamic"; // Don't cache API results
|
||||
|
||||
async function getPlans() {
|
||||
const params = { include: ["product"], perPage: 50 };
|
||||
|
||||
let hasNextPage = true;
|
||||
let page = 1;
|
||||
|
||||
let variants = [];
|
||||
let products = [];
|
||||
|
||||
while (hasNextPage) {
|
||||
const resp = await ls.getVariants(params);
|
||||
|
||||
variants = variants.concat(resp["data"]);
|
||||
products = products.concat(resp["included"]);
|
||||
|
||||
if (resp["meta"]["page"]["lastPage"] > page) {
|
||||
page += 1;
|
||||
params["page"] = page;
|
||||
} else {
|
||||
hasNextPage = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Nest products inside variants
|
||||
const prods = {};
|
||||
for (let i = 0; i < products.length; i++) {
|
||||
prods[products[i]["id"]] = products[i]["attributes"];
|
||||
}
|
||||
for (let i = 0; i < variants.length; i++) {
|
||||
variants[i]["product"] = prods[variants[i]["attributes"]["product_id"]];
|
||||
}
|
||||
}
|
||||
export default async function Page() {
|
||||
await getPlans();
|
||||
|
||||
return <p>Done!</p>;
|
||||
}
|
94
components/plan.jsx
Normal file
94
components/plan.jsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
/* /componens/plan.jsx */
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react'
|
||||
|
||||
function createMarkup(html) {
|
||||
return {__html: html}
|
||||
}
|
||||
|
||||
function formatPrice(price) {
|
||||
return price / 100
|
||||
}
|
||||
|
||||
function formatInterval(interval, intervalCount) {
|
||||
return intervalCount > 1 ? `${intervalCount} ${interval}s` : interval
|
||||
}
|
||||
|
||||
function IntervalSwitcher({ intervalValue, changeInterval }) {
|
||||
return (
|
||||
<div className="mt-6 flex justify-center items-center gap-4 text-sm text-gray-500">
|
||||
<div data-plan-toggle="month">
|
||||
Monthly
|
||||
</div>
|
||||
<div>
|
||||
<label className="toggle relative inline-block">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={intervalValue == 'year'}
|
||||
onChange={(e) => changeInterval(e.target.checked ? 'year' : 'month')}
|
||||
/>
|
||||
<span className="slider absolute rounded-full bg-gray-300 shadow-md"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div data-plan-toggle="year">
|
||||
Yearly
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Plan({ plan, subscription, intervalValue }) {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
'flex flex-col p-4 rounded-md border-solid border-2 border-gray-200'
|
||||
+ (plan.interval !== intervalValue ? ' hidden' : '')
|
||||
+ (subscription?.status !== 'expired' && subscription?.variantId == plan.variantId ? ' opacity-50' : '')
|
||||
}
|
||||
>
|
||||
<div className="grow">
|
||||
<h1 className="font-bold text-lg mb-1">{plan.variantName}</h1>
|
||||
<div dangerouslySetInnerHTML={createMarkup(plan.description)}></div>
|
||||
<div className="my-4">
|
||||
<span className="text-2xl">${formatPrice(plan.price)}</span>
|
||||
|
||||
<span className="text-gray-500">/{formatInterval(plan.interval, plan.intervalCount)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<a
|
||||
href="#"
|
||||
className="block text-center py-2 px-5 bg-amber-200 rounded-full font-bold text-amber-800 shadow-md shadow-gray-300/30 select-none"
|
||||
>
|
||||
Sign up
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Plans({ plans, subscription }) {
|
||||
|
||||
const [intervalValue, setIntervalValue] = useState('month')
|
||||
|
||||
return (
|
||||
<>
|
||||
<IntervalSwitcher intervalValue={intervalValue} changeInterval={setIntervalValue} />
|
||||
|
||||
<div className="mt-5 grid gap-6 sm:grid-cols-2">
|
||||
|
||||
{plans.map(plan => (
|
||||
<Plan plan={plan} subscription={subscription} intervalValue={intervalValue} key={plan.variantId} />
|
||||
))}
|
||||
|
||||
</div>
|
||||
|
||||
<p className="mt-8 text-gray-400 text-sm text-center">
|
||||
Payments are processed securely by Lemon Squeezy.
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -3,18 +3,6 @@ const nextConfig = {
|
|||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
// async rewrites() {
|
||||
// return [
|
||||
// {
|
||||
// source: "/api/v1/chat/completions", // 用户访问的路径
|
||||
// destination: "/api/chat", // 实际上被映射到的路径
|
||||
// },
|
||||
// {
|
||||
// source: "/api/paper", // 另一个用户访问的路径
|
||||
// destination: "/api/chat", // 同样被映射到 common-route
|
||||
// },
|
||||
// ];
|
||||
// },
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
|
|
14
package-lock.json
generated
14
package-lock.json
generated
|
@ -10,6 +10,7 @@
|
|||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@lemonsqueezy/lemonsqueezy.js": "^2.0.0",
|
||||
"@next/third-parties": "^14.1.0",
|
||||
"@reduxjs/toolkit": "^2.0.1",
|
||||
"@supabase/ssr": "latest",
|
||||
|
@ -185,6 +186,14 @@
|
|||
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
|
||||
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
|
||||
},
|
||||
"node_modules/@lemonsqueezy/lemonsqueezy.js": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@lemonsqueezy/lemonsqueezy.js/-/lemonsqueezy.js-2.0.0.tgz",
|
||||
"integrity": "sha512-eZcc463vc2qDoRHZE/NKi/wduryl1aHe2T+pVER2H2up8Ed5A4+IYK7KD2S5MPZwVUzVQGPmWJu6mPonh6xreQ==",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "14.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.4.tgz",
|
||||
|
@ -3521,6 +3530,11 @@
|
|||
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
|
||||
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
|
||||
},
|
||||
"@lemonsqueezy/lemonsqueezy.js": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@lemonsqueezy/lemonsqueezy.js/-/lemonsqueezy.js-2.0.0.tgz",
|
||||
"integrity": "sha512-eZcc463vc2qDoRHZE/NKi/wduryl1aHe2T+pVER2H2up8Ed5A4+IYK7KD2S5MPZwVUzVQGPmWJu6mPonh6xreQ=="
|
||||
},
|
||||
"@next/env": {
|
||||
"version": "14.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.4.tgz",
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@lemonsqueezy/lemonsqueezy.js": "^2.0.0",
|
||||
"@next/third-parties": "^14.1.0",
|
||||
"@reduxjs/toolkit": "^2.0.1",
|
||||
"@supabase/ssr": "latest",
|
||||
|
|
|
@ -23,6 +23,6 @@
|
|||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "app/api/api"],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "app/api/api", "app/billing/refresh-plans/page.jsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user