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: {
|
typescript: {
|
||||||
ignoreBuildErrors: true,
|
ignoreBuildErrors: true,
|
||||||
},
|
},
|
||||||
// async rewrites() {
|
|
||||||
// return [
|
|
||||||
// {
|
|
||||||
// source: "/api/v1/chat/completions", // 用户访问的路径
|
|
||||||
// destination: "/api/chat", // 实际上被映射到的路径
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// source: "/api/paper", // 另一个用户访问的路径
|
|
||||||
// destination: "/api/chat", // 同样被映射到 common-route
|
|
||||||
// },
|
|
||||||
// ];
|
|
||||||
// },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = nextConfig;
|
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/free-solid-svg-icons": "^6.5.1",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@juggle/resize-observer": "^3.4.0",
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
|
"@lemonsqueezy/lemonsqueezy.js": "^2.0.0",
|
||||||
"@next/third-parties": "^14.1.0",
|
"@next/third-parties": "^14.1.0",
|
||||||
"@reduxjs/toolkit": "^2.0.1",
|
"@reduxjs/toolkit": "^2.0.1",
|
||||||
"@supabase/ssr": "latest",
|
"@supabase/ssr": "latest",
|
||||||
|
@ -185,6 +186,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
|
||||||
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
|
"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": {
|
"node_modules/@next/env": {
|
||||||
"version": "14.0.4",
|
"version": "14.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.4.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
|
||||||
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
|
"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": {
|
"@next/env": {
|
||||||
"version": "14.0.4",
|
"version": "14.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.4.tgz",
|
"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/free-solid-svg-icons": "^6.5.1",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@juggle/resize-observer": "^3.4.0",
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
|
"@lemonsqueezy/lemonsqueezy.js": "^2.0.0",
|
||||||
"@next/third-parties": "^14.1.0",
|
"@next/third-parties": "^14.1.0",
|
||||||
"@reduxjs/toolkit": "^2.0.1",
|
"@reduxjs/toolkit": "^2.0.1",
|
||||||
"@supabase/ssr": "latest",
|
"@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"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user