diff --git a/app/billing/page1.tsx b/app/billing/page1.tsx new file mode 100644 index 0000000..b9fd355 --- /dev/null +++ b/app/billing/page1.tsx @@ -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 ( +//
+//

Billing

+ +// +//
+// ); +// } diff --git a/app/billing/refresh-plans/page.jsx b/app/billing/refresh-plans/page.jsx new file mode 100644 index 0000000..0255caa --- /dev/null +++ b/app/billing/refresh-plans/page.jsx @@ -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

Done!

; +} diff --git a/components/plan.jsx b/components/plan.jsx new file mode 100644 index 0000000..871c64b --- /dev/null +++ b/components/plan.jsx @@ -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 ( +
+
+ Monthly +
+
+ +
+
+ Yearly +
+
+ ); +} + +function Plan({ plan, subscription, intervalValue }) { + return ( +
+
+

{plan.variantName}

+
+
+ ${formatPrice(plan.price)} +   + /{formatInterval(plan.interval, plan.intervalCount)} +
+
+ +
+ + Sign up + +
+
+ ) +} + +export default function Plans({ plans, subscription }) { + + const [intervalValue, setIntervalValue] = useState('month') + + return ( + <> + + +
+ + {plans.map(plan => ( + + ))} + +
+ +

+ Payments are processed securely by Lemon Squeezy. +

+ + ) +} \ No newline at end of file diff --git a/next.config.js b/next.config.js index 4043a0e..e0b5fdf 100644 --- a/next.config.js +++ b/next.config.js @@ -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; diff --git a/package-lock.json b/package-lock.json index 2368f9e..f45d844 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index ed0e72c..484fc6f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/tsconfig.json b/tsconfig.json index a177f04..27e9a45 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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"] }