feat: lemonsqueezy1

This commit is contained in:
liuweiqing 2024-02-05 23:11:47 +08:00
parent c6c9914e84
commit fbd899cae3
7 changed files with 173 additions and 13 deletions

17
app/billing/page1.tsx Normal file
View 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>
// );
// }

View 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
View 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>
&nbsp;
<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>
</>
)
}

View File

@ -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
View File

@ -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",

View File

@ -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",

View File

@ -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"]
} }