mirror of
https://github.com/tabler/tabler.git
synced 2025-12-21 17:34:25 +04:00
Compare commits
26 Commits
main-rever
...
dev-site-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95891cb108 | ||
|
|
09a8e2d2c5 | ||
|
|
e4c76be517 | ||
|
|
9bbdba9c67 | ||
|
|
4ef2d125c2 | ||
|
|
7193a70102 | ||
|
|
442ac3bb4b | ||
|
|
877182140d | ||
|
|
9772160071 | ||
|
|
aeff172a41 | ||
|
|
7e62c3a563 | ||
|
|
d673851db5 | ||
|
|
020255f161 | ||
|
|
5edc93384c | ||
|
|
0efbb01e55 | ||
|
|
361e81e478 | ||
|
|
ebda434060 | ||
|
|
8ffe0e6a1a | ||
|
|
5250158600 | ||
|
|
e307ba44fb | ||
|
|
8cf5058456 | ||
|
|
b7c772ce1b | ||
|
|
1f0e6e074a | ||
|
|
a76df72359 | ||
|
|
90f4931c96 | ||
|
|
33bbc46229 |
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"presets": ["next/babel"],
|
||||
"plugins": []
|
||||
"plugins": ["@babel/plugin-transform-private-methods"]
|
||||
}
|
||||
|
||||
@@ -1,2 +1,47 @@
|
||||
# App
|
||||
NEXT_PUBLIC_APP_URL=http://localhost:3010
|
||||
# * Create an .env file with the following content:
|
||||
|
||||
# Created by Vercel CLI
|
||||
NEXT_PUBLIC_APP_URL=""
|
||||
NX_DAEMON=""
|
||||
TURBO_REMOTE_ONLY=""
|
||||
TURBO_RUN_SUMMARY=""
|
||||
VERCEL_ENV=""
|
||||
VERCEL_GIT_COMMIT_AUTHOR_LOGIN=""
|
||||
VERCEL_GIT_COMMIT_AUTHOR_NAME=""
|
||||
VERCEL_GIT_COMMIT_MESSAGE=""
|
||||
VERCEL_GIT_COMMIT_REF=""
|
||||
VERCEL_GIT_COMMIT_SHA=""
|
||||
VERCEL_GIT_PREVIOUS_SHA=""
|
||||
VERCEL_GIT_PROVIDER=""
|
||||
VERCEL_GIT_PULL_REQUEST_ID=""
|
||||
VERCEL_GIT_REPO_ID=""
|
||||
VERCEL_GIT_REPO_OWNER=""
|
||||
VERCEL_GIT_REPO_SLUG=""
|
||||
VERCEL_URL=""
|
||||
|
||||
# Postgres
|
||||
POSTGRES_URL=""
|
||||
POSTGRES_PRISMA_URL=""
|
||||
POSTGRES_URL_NON_POOLING=""
|
||||
POSTGRES_USER=""
|
||||
POSTGRES_HOST=""
|
||||
POSTGRES_PASSWORD=""
|
||||
POSTGRES_DATABASE=""
|
||||
|
||||
# Providers
|
||||
GITHUB_ID=""
|
||||
GITHUB_SECRET=""
|
||||
GOOGLE_ID=""
|
||||
GOOGLE_SECRET=""
|
||||
AUTH0_CLIENT_ID=""
|
||||
AUTH0_CLIENT_SECRET=""
|
||||
AUTH0_ISSUER=""
|
||||
|
||||
# Auth config
|
||||
NEXTAUTH_SECRET=""
|
||||
NEXTAUTH_URL=""
|
||||
|
||||
# Lemon squeezy
|
||||
LEMON_SQUEEZY_API_KEY=""
|
||||
LEMON_SQUEEZY_SIGNING_SECRET=""
|
||||
LEMON_SQUEEZY_STORE_ID=""
|
||||
|
||||
3
site/.gitignore
vendored
3
site/.gitignore
vendored
@@ -25,6 +25,7 @@ yarn-error.log*
|
||||
# local env files
|
||||
.env
|
||||
.env.local
|
||||
.env*.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
@@ -47,4 +48,4 @@ _site
|
||||
.contentlayer
|
||||
|
||||
public/static/tabler-icons/icons/*
|
||||
public/static/tabler-icons/icons-png/*
|
||||
public/static/tabler-icons/icons-png/*
|
||||
18
site/app/(entry)/layout.tsx
Normal file
18
site/app/(entry)/layout.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import EntryHeader from '@/components/layout/EntryHeader';
|
||||
|
||||
export default function CoreLayout({
|
||||
children
|
||||
}: {
|
||||
children: React.ReactNode,
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<EntryHeader />
|
||||
<div className="page page-center">
|
||||
<div className="container container-tight py-4">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
10
site/app/(entry)/signin/page.tsx
Normal file
10
site/app/(entry)/signin/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import Signin from '@/components/Signin';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Tabler Sign in',
|
||||
description: 'Sign in to Tabler',
|
||||
};
|
||||
|
||||
export default function SigninPage() {
|
||||
return <Signin/>;
|
||||
}
|
||||
10
site/app/(entry)/signup/page.tsx
Normal file
10
site/app/(entry)/signup/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import Signup from '@/components/Signup';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Tabler Sign up',
|
||||
description: 'Sign up to Tabler',
|
||||
};
|
||||
|
||||
export default function SignupPage() {
|
||||
return <Signup/>;
|
||||
}
|
||||
41
site/app/(marketing)/billing/change-plan/page.tsx
Normal file
41
site/app/(marketing)/billing/change-plan/page.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { getSession } from '@/lib/auth'
|
||||
import Link from 'next/link'
|
||||
import { PlansComponent } from '@/components/Manage'
|
||||
import { getPlans, getSubscription } from '@/lib/data'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export const metadata = {
|
||||
title: 'Change plan'
|
||||
}
|
||||
|
||||
export default async function Page() {
|
||||
const session = await getSession()
|
||||
|
||||
const sub = await getSubscription(session?.user?.id)
|
||||
|
||||
if (!sub) {
|
||||
redirect('/billing')
|
||||
}
|
||||
|
||||
const plans = await getPlans()
|
||||
|
||||
return (
|
||||
<section className="section">
|
||||
<div className="container">
|
||||
<h2 className="page-title text-center">Change plan</h2>
|
||||
|
||||
<Link href="/billing/" className="mb-6">← Back to billing</Link>
|
||||
|
||||
{sub.status == 'on_trial' && (
|
||||
<div className="my-8 p-4 h-subheader">
|
||||
You are currently on a free trial. You will not be charged when changing plans during a trial.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<PlansComponent plans={plans} sub={sub} />
|
||||
|
||||
<script src="https://app.lemonsqueezy.com/js/lemon.js" defer></script>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
28
site/app/(marketing)/billing/page.tsx
Normal file
28
site/app/(marketing)/billing/page.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { getPlans, getSubscription } from '@/lib/data'
|
||||
/* Full in-app billing component */
|
||||
import { SubscriptionComponent } from '@/components/Subscription'
|
||||
|
||||
export const metadata = {
|
||||
title: 'Billing'
|
||||
}
|
||||
|
||||
export default async function Page() {
|
||||
const session = await getSession()
|
||||
|
||||
const plans = await getPlans()
|
||||
|
||||
const sub = await getSubscription(session?.user?.id)
|
||||
|
||||
return (
|
||||
<section className="section">
|
||||
<div className="container">
|
||||
<h2 className="page-title text-center">Billing</h2>
|
||||
|
||||
<SubscriptionComponent sub={sub} plans={plans} />
|
||||
|
||||
<script src="https://app.lemonsqueezy.com/js/lemon.js" defer></script>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
107
site/app/(marketing)/billing/refresh-plans/page.tsx
Normal file
107
site/app/(marketing)/billing/refresh-plans/page.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import LemonSqueezy from '@lemonsqueezy/lemonsqueezy.js'
|
||||
|
||||
const ls = new LemonSqueezy(process.env.LEMON_SQUEEZY_API_KEY as string)
|
||||
|
||||
export const dynamic = 'force-dynamic' // Don't cache API results
|
||||
|
||||
async function getPlans() {
|
||||
|
||||
const params = { include: ['product'] as Array<'product' | 'files'>, perPage: 50 }
|
||||
|
||||
let hasNextPage = true;
|
||||
let page = 1;
|
||||
|
||||
let variants = [] as {}[]
|
||||
let products = [] as Record<string, any>
|
||||
|
||||
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']]
|
||||
}
|
||||
|
||||
// Save locally
|
||||
let variantId,
|
||||
variant,
|
||||
product,
|
||||
productId
|
||||
|
||||
for (let i = 0; i < variants.length; i++) {
|
||||
|
||||
variant = variants[i]
|
||||
|
||||
if ( !variant['attributes']['is_subscription'] ) {
|
||||
console.log('Not a subscription')
|
||||
continue
|
||||
}
|
||||
|
||||
if ( String(variant['product']['store_id']) !== process.env.LEMON_SQUEEZY_STORE_ID ) {
|
||||
console.log(`Store ID ${variant['product']['store_id']} does not match (${process.env.LEMON_SQUEEZY_STORE_ID})`)
|
||||
continue
|
||||
}
|
||||
|
||||
variantId = parseInt(variant['id'])
|
||||
product = variant['product']
|
||||
productId = parseInt(variant['attributes']['product_id'])
|
||||
|
||||
// Get variant's Price objects
|
||||
let prices = await ls.getPrices({ variantId: variantId, perPage: 100 })
|
||||
// The first object is the latest/current price
|
||||
let variant_price = prices['data'][0]['attributes']['unit_price']
|
||||
variant = variant['attributes']
|
||||
|
||||
const updateData = {
|
||||
productId: productId,
|
||||
name: product['name'],
|
||||
variantName: variant['name'],
|
||||
status: variant['status'],
|
||||
sort: variant['sort'],
|
||||
description: variant['description'],
|
||||
price: variant_price, // display price in the app matches current Price object in LS
|
||||
interval: variant['interval'],
|
||||
intervalCount: variant['interval_count'],
|
||||
}
|
||||
const createData = { ...updateData, variantId}
|
||||
|
||||
try {
|
||||
await prisma.plan.upsert({
|
||||
where: {
|
||||
variantId: variantId
|
||||
},
|
||||
update: updateData,
|
||||
create: createData
|
||||
})
|
||||
} catch (error) {
|
||||
console.log(variant)
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default async function Page() {
|
||||
await getPlans()
|
||||
|
||||
return (
|
||||
<p>
|
||||
Done!
|
||||
</p>
|
||||
)
|
||||
}
|
||||
152
site/app/(marketing)/billing/webhook/route.ts
Normal file
152
site/app/(marketing)/billing/webhook/route.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import LemonSqueezy from '@lemonsqueezy/lemonsqueezy.js'
|
||||
|
||||
const ls = new LemonSqueezy(process.env.LEMON_SQUEEZY_API_KEY as string)
|
||||
|
||||
|
||||
async function processEvent(event) {
|
||||
|
||||
let processingError = ''
|
||||
|
||||
const customData = event.body['meta']['custom_data'] || null
|
||||
|
||||
if (!customData || !customData['user_id']) {
|
||||
|
||||
processingError = 'No user ID, can\'t process'
|
||||
|
||||
} else {
|
||||
|
||||
const obj = event.body['data']
|
||||
|
||||
if (event.eventName.startsWith('subscription_payment_')) {
|
||||
// Save subscription invoices; obj is a "Subscription invoice"
|
||||
|
||||
/* Not implemented */
|
||||
|
||||
} else if (event.eventName.startsWith('subscription_')) {
|
||||
// Save subscriptions; obj is a "Subscription"
|
||||
|
||||
const data = obj['attributes']
|
||||
|
||||
// We assume the Plan table is up to date
|
||||
const plan = await prisma.plan.findUnique({
|
||||
where: {
|
||||
variantId: data['variant_id']
|
||||
},
|
||||
})
|
||||
|
||||
if (!plan) {
|
||||
|
||||
processingError = 'Plan not found in DB. Could not process webhook event.'
|
||||
|
||||
} else {
|
||||
|
||||
// Update the subscription
|
||||
|
||||
const lemonSqueezyId = parseInt(obj['id'])
|
||||
|
||||
// Get subscription's Price object
|
||||
// We save the Price value to the subscription so we can display it in the UI
|
||||
let priceData = await ls.getPrice({ id: data['first_subscription_item']['price_id'] })
|
||||
|
||||
const updateData = {
|
||||
orderId: data['order_id'],
|
||||
name: data['user_name'],
|
||||
email: data['user_email'],
|
||||
status: data['status'],
|
||||
renewsAt: data['renews_at'],
|
||||
endsAt: data['ends_at'],
|
||||
trialEndsAt: data['trial_ends_at'],
|
||||
planId: plan['id'],
|
||||
userId: customData['user_id'],
|
||||
price: priceData['data']['attributes']['unit_price'],
|
||||
subscriptionItemId: data['first_subscription_item']['id'],
|
||||
// Save this for usage-based billing reporting; no need to if you use quantity-based billing
|
||||
isUsageBased: data['first_subscription_item']['is_usage_based'],
|
||||
}
|
||||
|
||||
const createData = { ...updateData, lemonSqueezyId}
|
||||
createData.price = plan.price
|
||||
|
||||
try {
|
||||
// Create/update subscription
|
||||
await prisma.subscription.upsert({
|
||||
where: {
|
||||
lemonSqueezyId: lemonSqueezyId
|
||||
},
|
||||
update: updateData,
|
||||
create: createData,
|
||||
})
|
||||
} catch (error) {
|
||||
processingError = error
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} else if (event.eventName.startsWith('order_')) {
|
||||
// Save orders; obj is a "Order"
|
||||
|
||||
/* Not implemented */
|
||||
|
||||
} else if (event.eventName.startsWith('license_')) {
|
||||
// Save license keys; obj is a "License key"
|
||||
|
||||
/* Not implemented */
|
||||
|
||||
}
|
||||
|
||||
try {
|
||||
// Mark event as processed
|
||||
await prisma.webhookEvent.update({
|
||||
where: {
|
||||
id: event.id
|
||||
},
|
||||
data: {
|
||||
processed: true,
|
||||
processingError
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function POST(request: Request) {
|
||||
|
||||
// Make sure request is from Lemon Squeezy
|
||||
|
||||
const crypto = require('crypto')
|
||||
|
||||
const rawBody = await request.text()
|
||||
|
||||
const secret = process.env.LEMONSQUEEZY_WEBHOOK_SECRET
|
||||
const hmac = crypto.createHmac('sha256', secret)
|
||||
const digest = Buffer.from(hmac.update(rawBody).digest('hex'), 'utf8')
|
||||
const signature = Buffer.from(request.headers.get('X-Signature') || '', 'utf8')
|
||||
|
||||
if (!crypto.timingSafeEqual(digest, signature)) {
|
||||
throw new Error('Invalid signature.')
|
||||
}
|
||||
|
||||
// Now save the event
|
||||
|
||||
const data = JSON.parse(rawBody)
|
||||
|
||||
const event = await prisma.webhookEvent.create({
|
||||
data: {
|
||||
eventName: data['meta']['event_name'],
|
||||
body: data
|
||||
},
|
||||
})
|
||||
|
||||
// Process the event
|
||||
// This could be done out of the main thread
|
||||
|
||||
processEvent(event)
|
||||
|
||||
return new Response('Done');
|
||||
}
|
||||
5
site/app/api/auth/[...nextauth]/route.ts
Normal file
5
site/app/api/auth/[...nextauth]/route.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import NextAuth from 'next-auth';
|
||||
import { authConfig } from '@/lib/auth';
|
||||
|
||||
const authHandler = NextAuth(authConfig);
|
||||
export {authHandler as GET , authHandler as POST};
|
||||
55
site/app/api/checkouts/route.ts
Normal file
55
site/app/api/checkouts/route.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import LemonSqueezy from '@lemonsqueezy/lemonsqueezy.js'
|
||||
|
||||
const ls = new LemonSqueezy(process.env.LEMON_SQUEEZY_API_KEY as string)
|
||||
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const session = await getSession()
|
||||
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: true, message: 'Not logged in.' }, { status: 401 })
|
||||
}
|
||||
|
||||
const res = await request.json()
|
||||
|
||||
if ( !res.variantId ) {
|
||||
return NextResponse.json({ error: true, message: 'No variant ID was provided.' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Customise the checkout experience
|
||||
// All the options: https://docs.lemonsqueezy.com/api/checkouts#create-a-checkout
|
||||
const attributes = {
|
||||
'checkout_options': {
|
||||
'embed': true,
|
||||
'media': false,
|
||||
'button_color': '#fde68a'
|
||||
},
|
||||
'checkout_data': {
|
||||
'email': session.user?.email, // Displays in the checkout form
|
||||
'custom': {
|
||||
'user_id': session.user?.id // Sent in the background; visible in webhooks and API calls
|
||||
}
|
||||
},
|
||||
'product_options': {
|
||||
'enabled_variants': [res.variantId], // Only show the selected variant in the checkout
|
||||
'redirect_url': `${process.env.NEXT_PUBLIC_APP_URL}/billing/`,
|
||||
'receipt_link_url': `${process.env.NEXT_PUBLIC_APP_URL}/billing/`,
|
||||
'receipt_button_text': 'Go to your account',
|
||||
'receipt_thank_you_note': 'Thank you for signing up to Lemonstand!'
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const checkout = await ls.createCheckout({
|
||||
storeId: Number(process.env.LEMON_SQUEEZY_STORE_ID),
|
||||
variantId: res.variantId,
|
||||
attributes
|
||||
})
|
||||
|
||||
return NextResponse.json({'error': false, 'url': checkout['data']['attributes']['url']}, {status: 200})
|
||||
} catch (e) {
|
||||
return NextResponse.json({'error': true, 'message': e.message}, {status: 400})
|
||||
}
|
||||
}
|
||||
42
site/app/api/register/route.ts
Normal file
42
site/app/api/register/route.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
// import prisma from '@/lib/prisma';
|
||||
// import { hash } from 'bcrypt';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function POST(req: Request) {
|
||||
// try {
|
||||
// const { name, email, password } = (await req.json()) as {
|
||||
// name: string;
|
||||
// email: string;
|
||||
// password: string;
|
||||
// };
|
||||
|
||||
// if (!name || !email || !password) {
|
||||
// throw { message: 'all fields are required' };
|
||||
// }
|
||||
|
||||
// const hashedPassword = await hash(password, 12);
|
||||
|
||||
// const user = await prisma.user.create({
|
||||
// data: {
|
||||
// name,
|
||||
// email: email.toLowerCase(),
|
||||
// password: hashedPassword,
|
||||
// },
|
||||
// });
|
||||
|
||||
// return NextResponse.json({
|
||||
// user: {
|
||||
// name: user.name,
|
||||
// email: user.email,
|
||||
// },
|
||||
// });
|
||||
// } catch (error: any) {
|
||||
return new NextResponse(
|
||||
JSON.stringify({
|
||||
status: 'error',
|
||||
// message: error.message,
|
||||
}),
|
||||
{ status: 500 }
|
||||
);
|
||||
// }
|
||||
}
|
||||
121
site/app/api/subscriptions/[id]/route.ts
Normal file
121
site/app/api/subscriptions/[id]/route.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getPlan } from '@/lib/data'
|
||||
import LemonSqueezy from '@lemonsqueezy/lemonsqueezy.js'
|
||||
|
||||
const ls = new LemonSqueezy(process.env.LEMON_SQUEEZY_API_KEY as string)
|
||||
|
||||
export async function GET(_: Request, { params }: { params: { id: string } }) {
|
||||
/**
|
||||
* Used by some buttons to get subscription update billing and customer portal URLs
|
||||
*/
|
||||
try {
|
||||
const subscription = await ls.getSubscription({ id: Number(params.id) })
|
||||
return NextResponse.json({ error: false, subscription: {
|
||||
update_billing_url: subscription['data']['attributes']['urls']['update_payment_method'],
|
||||
customer_portal_url: subscription['data']['attributes']['urls']['customer_portal']
|
||||
} }, { status: 200 })
|
||||
} catch (e) {
|
||||
return NextResponse.json({ error: true, message: e.message }, { status: 400 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request, { params }: { params: { id: string } }) {
|
||||
|
||||
const res = await request.json()
|
||||
|
||||
let subscription
|
||||
|
||||
if (res.variantId && res.productId) {
|
||||
|
||||
// Update plan
|
||||
|
||||
try {
|
||||
subscription = await ls.updateSubscription({
|
||||
id: Number(params.id),
|
||||
productId: res.productId,
|
||||
variantId: res.variantId,
|
||||
})
|
||||
} catch (e) {
|
||||
return NextResponse.json({ error: true, message: e.message }, { status: 400 })
|
||||
}
|
||||
|
||||
} else if (res.action == 'resume') {
|
||||
|
||||
// Resume
|
||||
|
||||
try {
|
||||
subscription = await ls.resumeSubscription({ id: Number(params.id) })
|
||||
} catch (e) {
|
||||
return NextResponse.json({ error: true, message: e.message }, { status: 400 })
|
||||
}
|
||||
|
||||
} else if (res.action == 'cancel') {
|
||||
|
||||
// Cancel
|
||||
|
||||
try {
|
||||
subscription = await ls.cancelSubscription({ id: Number(params.id) })
|
||||
} catch (e) {
|
||||
return NextResponse.json({ error: true, message: e.message }, { status: 400 })
|
||||
}
|
||||
|
||||
} else if (res.action == 'pause') {
|
||||
|
||||
// Pause
|
||||
|
||||
try {
|
||||
subscription = await ls.pauseSubscription({ id: Number(params.id) })
|
||||
} catch (e) {
|
||||
return NextResponse.json({ error: true, message: e.message }, { status: 400 })
|
||||
}
|
||||
|
||||
} else if (res.action == 'unpause') {
|
||||
|
||||
// Unpause
|
||||
|
||||
try {
|
||||
subscription = await ls.unpauseSubscription({ id: Number(params.id) })
|
||||
} catch (e) {
|
||||
return NextResponse.json({ error: true, message: e.message }, { status: 400 })
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// Missing data in request
|
||||
|
||||
return NextResponse.json({ error: true, message: 'Valid data not found.' }, { status: 400 })
|
||||
|
||||
}
|
||||
|
||||
// Return values needed to refresh state in UI
|
||||
// DB will be updated in the background with webhooks
|
||||
|
||||
// Get price
|
||||
let resp = await ls.getPrice({ id: subscription['data']['attributes']['first_subscription_item']['price_id'] })
|
||||
let subItemPrice = resp['data']['attributes']['unit_price']
|
||||
|
||||
// Return a filtered subscription object to the UI
|
||||
const sub = {
|
||||
product_id: subscription['data']['attributes']['product_id'],
|
||||
variant_id: subscription['data']['attributes']['variant_id'],
|
||||
status: subscription['data']['attributes']['status'],
|
||||
card_brand: subscription['data']['attributes']['card_brand'],
|
||||
card_last_four: subscription['data']['attributes']['card_last_four'],
|
||||
trial_ends_at: subscription['data']['attributes']['trial_ends_at'],
|
||||
renews_at: subscription['data']['attributes']['renews_at'],
|
||||
ends_at: subscription['data']['attributes']['ends_at'],
|
||||
resumes_at: subscription['data']['attributes']['resumes_at'],
|
||||
plan: {},
|
||||
price: subItemPrice,
|
||||
}
|
||||
|
||||
// Get plan's data
|
||||
const plan = await getPlan(sub.variant_id)
|
||||
sub.plan = {
|
||||
interval: plan?.interval,
|
||||
name: plan?.variantName
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: false, subscription: sub }, { status: 200 })
|
||||
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import '@/styles/website.scss';
|
||||
|
||||
import { creator, description, name, uiUrl } from '@/config/site';
|
||||
import PageProgress from '@/components/PageProgress';
|
||||
import { NextAuthProvider } from '@/components/NextAuthProvider';
|
||||
|
||||
export const metadata = {
|
||||
metadataBase: uiUrl,
|
||||
@@ -75,7 +76,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
</head>
|
||||
<body className="body-gradient">
|
||||
<PageProgress />
|
||||
{children}
|
||||
<NextAuthProvider>{children}</NextAuthProvider>
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||
<script src="https://assets.lemonsqueezy.com/lemon.js" defer />
|
||||
</body>
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import { MetadataRoute } from 'next';
|
||||
import { allDocs, allGuides, allPages, allPosts } from '@/.contentlayer/generated';
|
||||
|
||||
|
||||
|
||||
import { uiUrl } from '@/config/site';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const pages = [
|
||||
'testimonials',
|
||||
'support',
|
||||
|
||||
@@ -33,6 +33,23 @@ const icons = {
|
||||
>
|
||||
<path d="M9 19c-4.3 1.4 -4.3 -2.5 -6 -3m12 5v-3.5c0 -1 .1 -1.4 -.5 -2c2.8 -.3 5.5 -1.4 5.5 -6a4.6 4.6 0 0 0 -1.3 -3.2a4.2 4.2 0 0 0 -.1 -3.2s-1.1 -.3 -3.5 1.3a12.3 12.3 0 0 0 -6.2 0c-2.4 -1.6 -3.5 -1.3 -3.5 -1.3a4.2 4.2 0 0 0 -.1 3.2a4.6 4.6 0 0 0 -1.3 3.2c0 4.6 2.7 5.7 5.5 6c-.6 .6 -.6 1.2 -.5 2v3.5" />
|
||||
</svg>
|
||||
),
|
||||
'brand-google': ({ className }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={clsx('icon icon-tabler icon-tabler-brand-google', className)}
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M17.788 5.108a9 9 0 1 0 3.212 6.892h-8"></path>
|
||||
</svg>
|
||||
),
|
||||
'brand-sketch': ({ className }) => (
|
||||
<svg
|
||||
@@ -861,6 +878,44 @@ const icons = {
|
||||
<path d="M14 4h6v4h-6z" />
|
||||
</svg>
|
||||
),
|
||||
'logout': ({ className }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={clsx('icon icon-tabler icon-tabler-logout', className)}
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M14 8v-2a2 2 0 0 0 -2 -2h-7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2 -2v-2"></path>
|
||||
<path d="M9 12h12l-3 -3"></path>
|
||||
<path d="M18 15l3 -3"></path>
|
||||
</svg>
|
||||
),
|
||||
'rocket': ({ className }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={clsx('icon icon-tabler icon-tabler-rocket', className)}
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M4 13a8 8 0 0 1 7 7a6 6 0 0 0 3 -5a9 9 0 0 0 6 -8a3 3 0 0 0 -3 -3a9 9 0 0 0 -8 6a6 6 0 0 0 -5 3"></path>
|
||||
<path d="M7 14a6 6 0 0 0 -3 6a6 6 0 0 0 6 -3"></path>
|
||||
<path d="M15 9m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
|
||||
</svg>
|
||||
),
|
||||
'sun-moon': ({ className }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
292
site/components/Manage.tsx
Normal file
292
site/components/Manage.tsx
Normal file
@@ -0,0 +1,292 @@
|
||||
'use client'
|
||||
|
||||
import { useState, MouseEvent, Dispatch, SetStateAction } from 'react'
|
||||
import PlanCards from '@/components/Plan'
|
||||
import { Plan, Subscription, SubscriptionState } from '@/types'
|
||||
import clsx from 'clsx'
|
||||
|
||||
export const UpdateBillingLink = ({ subscription, elementType }:
|
||||
{ subscription: SubscriptionState, elementType?: string }
|
||||
) => {
|
||||
|
||||
const [isMutating, setIsMutating] = useState(false)
|
||||
|
||||
async function openUpdateModal(e: MouseEvent<HTMLAnchorElement>) {
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
setIsMutating(true)
|
||||
|
||||
/* Send request */
|
||||
const res = await fetch(`/api/subscriptions/${subscription?.id}`)
|
||||
const result = await res.json()
|
||||
if (result.error) {
|
||||
alert(result.message)
|
||||
setIsMutating(false)
|
||||
|
||||
} else {
|
||||
|
||||
LemonSqueezy.Url.Open(result.subscription.update_billing_url)
|
||||
setIsMutating(false)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (elementType == 'button') {
|
||||
return (
|
||||
<a
|
||||
href=""
|
||||
onClick={openUpdateModal}
|
||||
className={clsx('px-6 py-2 font-bold btn btn-block', {
|
||||
disabled: isMutating
|
||||
})}
|
||||
>
|
||||
Update your payment method
|
||||
</a>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<a
|
||||
href=""
|
||||
onClick={openUpdateModal}
|
||||
className={clsx('mb-2', {
|
||||
disabled: isMutating
|
||||
})}
|
||||
>
|
||||
Update your payment method
|
||||
</a>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const CancelLink = ({ subscription, setSubscription }:
|
||||
{ subscription: SubscriptionState, setSubscription: Dispatch<SetStateAction<SubscriptionState>> }
|
||||
) => {
|
||||
|
||||
const [isMutating, setIsMutating] = useState(false)
|
||||
|
||||
async function handleCancel(e: MouseEvent<HTMLAnchorElement>) {
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
if (confirm(`Please confirm you want to cancel your subscription.`)) {
|
||||
|
||||
setIsMutating(true)
|
||||
|
||||
/* Send request */
|
||||
const res = await fetch(`/api/subscriptions/${subscription?.id}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
action: 'cancel'
|
||||
})
|
||||
})
|
||||
const result = await res.json()
|
||||
if (result.error) {
|
||||
alert(result.message)
|
||||
setIsMutating(false)
|
||||
|
||||
} else {
|
||||
|
||||
setSubscription({
|
||||
...subscription,
|
||||
status: result['subscription']['status'],
|
||||
expiryDate: result['subscription']['ends_at'],
|
||||
})
|
||||
|
||||
alert('Your subscription has been cancelled.')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href=""
|
||||
onClick={handleCancel}
|
||||
className={clsx('mb-2', {
|
||||
disabled: isMutating
|
||||
})}
|
||||
>
|
||||
Cancel
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
export const ResumeButton = ({ subscription, setSubscription }:
|
||||
{ subscription: SubscriptionState, setSubscription: Dispatch<SetStateAction<SubscriptionState>> }
|
||||
) => {
|
||||
|
||||
const [isMutating, setIsMutating] = useState(false)
|
||||
|
||||
const resumeSubscription = async (e: MouseEvent<HTMLAnchorElement>) => {
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
if (confirm(`Please confirm you want to resume your subscription. You will be charged the regular subscription fee.`)) {
|
||||
|
||||
setIsMutating(true)
|
||||
|
||||
/* Send request */
|
||||
const res = await fetch(`/api/subscriptions/${subscription?.id}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
action: 'resume'
|
||||
})
|
||||
})
|
||||
const result = await res.json()
|
||||
if (result.error) {
|
||||
alert(result.message)
|
||||
setIsMutating(false)
|
||||
} else {
|
||||
|
||||
setSubscription({
|
||||
...subscription,
|
||||
status: result['subscription']['status'],
|
||||
renewalDate: result['subscription']['renews_at'],
|
||||
})
|
||||
|
||||
alert('Your subscription is now active again!.')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href=""
|
||||
onClick={resumeSubscription}
|
||||
className={clsx('px-6 py-2 font-bold btn btn-block', {
|
||||
disabled: isMutating
|
||||
})}
|
||||
>
|
||||
Resume your subscription
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
export const PauseLink = ({ subscription, setSubscription }:
|
||||
{ subscription: SubscriptionState, setSubscription: Dispatch<SetStateAction<SubscriptionState>> }
|
||||
) => {
|
||||
|
||||
const [isMutating, setIsMutating] = useState(false)
|
||||
|
||||
async function handlePause(e: MouseEvent<HTMLAnchorElement>) {
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
if (confirm(`Please confirm you want to pause your subscription.`)) {
|
||||
|
||||
setIsMutating(true)
|
||||
|
||||
/* Send request */
|
||||
const res = await fetch(`/api/subscriptions/${subscription?.id}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
action: 'pause'
|
||||
})
|
||||
})
|
||||
const result = await res.json()
|
||||
if (result.error) {
|
||||
alert(result.message)
|
||||
setIsMutating(false)
|
||||
|
||||
} else {
|
||||
|
||||
setSubscription({
|
||||
...subscription,
|
||||
status: result['subscription']['status'],
|
||||
unpauseDate: result['subscription']['resumes_at'],
|
||||
})
|
||||
|
||||
alert('Your subscription has been paused.')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href=""
|
||||
className={clsx('mb-2', {
|
||||
disabled: isMutating
|
||||
})}
|
||||
onClick={handlePause}
|
||||
>
|
||||
Pause payments
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
export const UnpauseButton = ({ subscription, setSubscription }:
|
||||
{ subscription: SubscriptionState, setSubscription: Dispatch<SetStateAction<SubscriptionState>> }
|
||||
) => {
|
||||
|
||||
const [isMutating, setIsMutating] = useState(false)
|
||||
|
||||
const unpauseSubscription = async (e: MouseEvent<HTMLAnchorElement>) => {
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
if (confirm(`Please confirm you want to unpause your subscription. Your payments will reactivate on their original schedule.`)) {
|
||||
|
||||
setIsMutating(true)
|
||||
|
||||
/* Send request */
|
||||
const res = await fetch(`/api/subscriptions/${subscription?.id}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
action: 'unpause'
|
||||
})
|
||||
})
|
||||
const result = await res.json()
|
||||
if (result.error) {
|
||||
alert(result.message)
|
||||
setIsMutating(false)
|
||||
} else {
|
||||
|
||||
setSubscription({
|
||||
...subscription,
|
||||
status: result['subscription']['status'],
|
||||
renewalDate: result['subscription']['renews_at'],
|
||||
})
|
||||
|
||||
alert('Your subscription is now active again!')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<a href=""
|
||||
onClick={unpauseSubscription}
|
||||
className={clsx('px-6 py-2 font-bold btn btn-block', {
|
||||
disabled: isMutating
|
||||
})}
|
||||
>
|
||||
Unpause your subscription
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
export const PlansComponent = ({ plans, sub }:
|
||||
{ plans: Plan[], sub: Subscription }
|
||||
) => {
|
||||
|
||||
const [subscription, setSubscription] = useState<SubscriptionState>(() => {
|
||||
if (sub) {
|
||||
return {
|
||||
id: sub.lemonSqueezyId,
|
||||
planName: sub.plan?.variantName,
|
||||
planInterval: sub.plan?.interval,
|
||||
productId: sub.plan?.productId,
|
||||
variantId: sub.plan?.variantId,
|
||||
status: sub.status,
|
||||
renewalDate: sub.renewsAt,
|
||||
trialEndDate: sub.trialEndsAt,
|
||||
expiryDate: sub.endsAt,
|
||||
}
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<PlanCards plans={plans} subscription={subscription} setSubscription={setSubscription} />
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import Link from '@/components/Link';
|
||||
import clsx from 'clsx';
|
||||
|
||||
function NavLink({ href, active, children, ...props }) {
|
||||
function NavLink({ href, active = false, children, ...props }) {
|
||||
if (active) {
|
||||
props.className = clsx(props.className, 'active');
|
||||
}
|
||||
|
||||
11
site/components/NextAuthProvider.tsx
Normal file
11
site/components/NextAuthProvider.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { SessionProvider } from 'next-auth/react';
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const NextAuthProvider = ({ children }: Props) => {
|
||||
return <SessionProvider>{children}</SessionProvider>;
|
||||
};
|
||||
122
site/components/Plan.tsx
Normal file
122
site/components/Plan.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, Dispatch, SetStateAction } from 'react'
|
||||
import PlanButton from '@/components/PlanButton'
|
||||
import { isPlanFeatured } from '@/helpers';
|
||||
import clsx from 'clsx';
|
||||
import Icon from '@/components/Icon';
|
||||
import type { Plan, SubscriptionState } from '@/types';
|
||||
|
||||
function formatPrice(price: number) {
|
||||
const priceString = price.toString()
|
||||
const dollars = priceString.substring(0, priceString.length-2)
|
||||
const cents = priceString.substring(priceString.length-2)
|
||||
if (cents === '00') return dollars
|
||||
return `${dollars}.${cents}`
|
||||
}
|
||||
|
||||
const formatDescription = (description?: string) => {
|
||||
if (!description) return;
|
||||
const pricingFeatures = description
|
||||
.replaceAll('<p>','')
|
||||
.replaceAll('</p>','')
|
||||
.split('<br>')
|
||||
return <ul className="pricing-features">
|
||||
{
|
||||
pricingFeatures.map((pricingFeature) => (
|
||||
<li key={pricingFeature}>
|
||||
<Icon name="check" className="text-green mr-2" />
|
||||
{pricingFeature}
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
|
||||
const IntervalSwitcher = ({ intervalValue, changeInterval }:
|
||||
{ intervalValue: string, changeInterval: Dispatch<SetStateAction<string>> }
|
||||
) => {
|
||||
return (
|
||||
<div className="text-center mb-5">
|
||||
<span className="mr-2">Monthly</span>
|
||||
<label className="form-switch">
|
||||
<input
|
||||
className="form-switch-input"
|
||||
type="checkbox"
|
||||
checked={intervalValue == 'year'}
|
||||
onChange={(e) => changeInterval(e.target.checked ? 'year' : 'month')}
|
||||
/>
|
||||
<span className="slider"/>
|
||||
</label>
|
||||
<span className="ml-2">Yearly</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const PlanCard = ({ plan, subscription, intervalValue, setSubscription }:
|
||||
{ plan: Plan, subscription: SubscriptionState, intervalValue: string, setSubscription: Dispatch<SetStateAction<SubscriptionState>> }
|
||||
) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx({
|
||||
'featured': isPlanFeatured(plan),
|
||||
'pricing-card': plan.interval === intervalValue,
|
||||
'visually-hidden': plan.interval !== intervalValue,
|
||||
})}
|
||||
>
|
||||
{
|
||||
isPlanFeatured(plan) &&
|
||||
<div className="pricing-label">
|
||||
<div className="label label-primary label-sm">Popular</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<h4 className="pricing-title">{plan.variantName}</h4>
|
||||
|
||||
<div className="pricing-price">
|
||||
<span className="pricing-price-currency">$</span>
|
||||
{ formatPrice(plan.price) }
|
||||
<div className="pricing-price-description">
|
||||
<div>per {plan.interval}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{formatDescription(plan.description || '')}
|
||||
|
||||
<PlanButton plan={plan} subscription={subscription} setSubscription={setSubscription} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const PlanCards = ({ plans, subscription, setSubscription }:
|
||||
{ plans: Plan[], subscription?: SubscriptionState, setSubscription: Dispatch<SetStateAction<SubscriptionState>> }
|
||||
) => {
|
||||
|
||||
const [intervalValue, setIntervalValue] = useState('month')
|
||||
|
||||
// Make sure Lemon.js is loaded
|
||||
useEffect(() => {
|
||||
window.createLemonSqueezy()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<IntervalSwitcher intervalValue={intervalValue} changeInterval={setIntervalValue} />
|
||||
|
||||
<div className="pricing">
|
||||
|
||||
{plans.map(plan => (
|
||||
<PlanCard plan={plan} subscription={subscription} intervalValue={intervalValue} key={plan.variantId} setSubscription={setSubscription} />
|
||||
))}
|
||||
|
||||
</div>
|
||||
|
||||
<p className="h-subheader mt-8 text-center">
|
||||
Payments are processed securely by Lemon Squeezy.
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PlanCards
|
||||
124
site/components/PlanButton.tsx
Normal file
124
site/components/PlanButton.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
'use client'
|
||||
|
||||
import { Dispatch, SetStateAction, useState, MouseEvent } from 'react'
|
||||
import clsx from 'clsx';
|
||||
import { isPlanFeatured } from '@/helpers';
|
||||
import { Plan, SubscriptionState } from '@/types';
|
||||
|
||||
const PlanButton = ({ plan, subscription, setSubscription }:
|
||||
{ plan: Plan, subscription: SubscriptionState, setSubscription: Dispatch<SetStateAction<SubscriptionState>> }
|
||||
) => {
|
||||
|
||||
const [isMutating, setIsMutating] = useState(false)
|
||||
|
||||
const createCheckout = async (e: MouseEvent<HTMLAnchorElement>, variantId: number) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (isMutating) return
|
||||
|
||||
setIsMutating(true)
|
||||
|
||||
// Create a checkout
|
||||
const res = await fetch('/api/checkouts', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
variantId: variantId
|
||||
})
|
||||
})
|
||||
const checkout = await res.json()
|
||||
if (checkout.error) {
|
||||
alert(checkout.message)
|
||||
} else {
|
||||
LemonSqueezy.Url.Open(checkout['url'])
|
||||
}
|
||||
|
||||
setIsMutating(false)
|
||||
}
|
||||
|
||||
const changePlan = async (e: MouseEvent<HTMLAnchorElement>, subscription: SubscriptionState, plan: Plan) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (isMutating) return
|
||||
|
||||
if (confirm(`Please confirm you want to change to the ${plan.variantName} ${plan.interval}ly plan. \
|
||||
For upgrades you will be charged a prorated amount.`)) {
|
||||
|
||||
setIsMutating(true)
|
||||
|
||||
// Send request
|
||||
const res = await fetch(`/api/subscriptions/${subscription?.id}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
variantId: plan.variantId,
|
||||
productId: plan.productId
|
||||
})
|
||||
})
|
||||
const result = await res.json()
|
||||
if (result.error) {
|
||||
alert(result.message)
|
||||
} else {
|
||||
setSubscription({
|
||||
...subscription,
|
||||
productId: result['subscription']['product_id'],
|
||||
variantId: result['subscription']['variant_id'],
|
||||
planName: result['subscription']['plan']['name'],
|
||||
planInterval: result['subscription']['plan']['interval'],
|
||||
status: result['subscription']['status'],
|
||||
renewalDate: result['subscription']['renews_at'],
|
||||
price: result['subscription']['price']
|
||||
})
|
||||
|
||||
alert('Your subscription plan has changed!')
|
||||
|
||||
// Webhooks will update the DB in the background
|
||||
}
|
||||
|
||||
setIsMutating(false)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{(!subscription || subscription.status == 'expired') ? (
|
||||
// Show a "Sign up" button to customers with no subscription
|
||||
|
||||
<div className="pricing-btn">
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => createCheckout(e, plan.variantId)}
|
||||
className={clsx('btn btn-block', {
|
||||
'btn-primary': isPlanFeatured(plan),
|
||||
disabled: isMutating
|
||||
})}
|
||||
>
|
||||
Sign up
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{subscription?.variantId == plan.variantId ? (
|
||||
<div className="pricing-btn">
|
||||
<span className="font-bold select-none">Your current plan</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="pricing-btn">
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => changePlan(e, subscription, plan)}
|
||||
className={clsx('btn btn-block', {
|
||||
'btn-primary': isPlanFeatured(plan),
|
||||
disabled: isMutating
|
||||
})}
|
||||
>
|
||||
Change to this plan
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PlanButton
|
||||
154
site/components/Signin.tsx
Normal file
154
site/components/Signin.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
'use client';
|
||||
|
||||
import Link from '@/components/Link';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import Icon from '@/components/Icon';
|
||||
import { signIn } from 'next-auth/react';
|
||||
import { useState } from 'react';
|
||||
import { getNextAuthErrorMessage } from '@/helpers/index';
|
||||
|
||||
export default function Signin() {
|
||||
// const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
// const [loading, setLoading] = useState(false);
|
||||
// const [formValues, setFormValues] = useState({
|
||||
// email: '',
|
||||
// password: '',
|
||||
// });
|
||||
const nextAuthError = searchParams.get('error');
|
||||
const [error] = useState(nextAuthError ? getNextAuthErrorMessage(nextAuthError) : '');
|
||||
const callbackUrl = searchParams.get('callbackUrl') || '/';
|
||||
|
||||
// const onSubmit = async (e: React.FormEvent) => {
|
||||
// e.preventDefault();
|
||||
// try {
|
||||
// setLoading(true);
|
||||
// setFormValues({ email: '', password: '' });
|
||||
|
||||
// const res = await signIn('credentials', {
|
||||
// redirect: false,
|
||||
// email: formValues.email,
|
||||
// password: formValues.password,
|
||||
// callbackUrl,
|
||||
// });
|
||||
|
||||
// setLoading(false);
|
||||
|
||||
// if (!res?.error) {
|
||||
// router.push(callbackUrl);
|
||||
// } else {
|
||||
// setError('Invalid email or password');
|
||||
// }
|
||||
// } catch (error: any) {
|
||||
// setLoading(false);
|
||||
// setError(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
// const { name, value } = event.target;
|
||||
// setFormValues({ ...formValues, [name]: value });
|
||||
// };
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-center mb-4">
|
||||
<Link href="/" className="mx-auto d-inline-block logo" aria-label="Tabler" />
|
||||
</div>
|
||||
<div className="flex-column card card-md">
|
||||
{/* <div className="card-body">
|
||||
<h2 className="h2 text-center mb-4">Login to your account</h2>
|
||||
{
|
||||
error &&
|
||||
<p className="text-center" style={{color: 'red'}}>{error}</p>
|
||||
}
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Email address</label>
|
||||
<input
|
||||
required
|
||||
type="email"
|
||||
name="email"
|
||||
value={formValues.email}
|
||||
onChange={handleChange}
|
||||
className="form-control"
|
||||
placeholder="your@email.com"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<label className="form-label">
|
||||
Password
|
||||
</label>
|
||||
<div className="input-group input-group-flat">
|
||||
<input
|
||||
required
|
||||
type="password"
|
||||
name="password"
|
||||
value={formValues.password}
|
||||
onChange={handleChange}
|
||||
className="form-control"
|
||||
placeholder="Your password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-footer">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary w-100"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'loading...' : 'Sign In'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>*/}
|
||||
|
||||
<div className="card-body">
|
||||
<h2 className="h2 text-center mb-4">Login to your account</h2>
|
||||
{
|
||||
error &&
|
||||
<p className="text-center" style={{color: 'red'}}>{error}</p>
|
||||
}
|
||||
<div className="mb-2">
|
||||
<label className="form-label text-center">
|
||||
Using email and password
|
||||
</label>
|
||||
<div className="form-footer">
|
||||
<button
|
||||
className="btn btn-primary w-100"
|
||||
onClick={() => signIn('auth0', { callbackUrl })}
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hr-text">or</div>
|
||||
|
||||
<div className="card-body">
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<a onClick={() => signIn('github', { callbackUrl })} className="btn w-100">
|
||||
<Icon name="brand-github"/>
|
||||
Login with Github
|
||||
</a>
|
||||
</div>
|
||||
<div className="col">
|
||||
<a onClick={() => signIn('google', { callbackUrl })} className="btn w-100">
|
||||
<Icon name="brand-google"/>
|
||||
Login with Google
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="text-center text-secondary mt-3">
|
||||
Don't have account yet?
|
||||
<a className="ml-2" onClick={() => router.push('/signup')}>
|
||||
Sign up
|
||||
</a>
|
||||
</div> */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
113
site/components/Signup.tsx
Normal file
113
site/components/Signup.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
'use client';
|
||||
|
||||
// import Link from '@/components/Link';
|
||||
// import { useRouter } from 'next/navigation';
|
||||
// import { ChangeEvent, useState } from 'react';
|
||||
// import { signIn } from 'next-auth/react';
|
||||
|
||||
export default function Signup() {
|
||||
// const router = useRouter();
|
||||
// const [loading, setLoading] = useState(false);
|
||||
// const [formValues, setFormValues] = useState({
|
||||
// name: '',
|
||||
// email: '',
|
||||
// password: '',
|
||||
// });
|
||||
// const [error, setError] = useState('');
|
||||
|
||||
// const onSubmit = async (e: React.FormEvent) => {
|
||||
// e.preventDefault();
|
||||
// setLoading(true);
|
||||
// setFormValues({ name: '', email: '', password: '' });
|
||||
|
||||
// try {
|
||||
// const res = await fetch('/api/register', {
|
||||
// method: 'POST',
|
||||
// body: JSON.stringify(formValues),
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// },
|
||||
// });
|
||||
|
||||
// setLoading(false);
|
||||
// if (!res.ok) {
|
||||
// setError((await res.json()).message);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// signIn(undefined, { callbackUrl: '/' });
|
||||
// } catch (error: any) {
|
||||
// setLoading(false);
|
||||
// setError(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
// const { name, value } = event.target;
|
||||
// setFormValues({ ...formValues, [name]: value });
|
||||
// };
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <div className="text-center mb-4">
|
||||
<Link href="/" className="mx-auto d-inline-block logo" aria-label="Tabler" />
|
||||
</div>
|
||||
<form onSubmit={onSubmit} className="flex-column card card-md">
|
||||
<div className="card-body">
|
||||
<h2 className="card-title text-center mb-4">Create new account</h2>
|
||||
{
|
||||
error &&
|
||||
<p className="text-center" style={{color: 'red'}}>{error}</p>
|
||||
}
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Name</label>
|
||||
<input
|
||||
required
|
||||
type="name"
|
||||
name="name"
|
||||
value={formValues.name}
|
||||
onChange={handleChange}
|
||||
className="form-control"
|
||||
placeholder="Name"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Email address</label>
|
||||
<input
|
||||
required
|
||||
type="email"
|
||||
name="email"
|
||||
value={formValues.email}
|
||||
onChange={handleChange}
|
||||
className="form-control"
|
||||
placeholder="Enter email"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Password</label>
|
||||
<div className="input-group input-group-flat">
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
value={formValues.password}
|
||||
onChange={handleChange}
|
||||
className="form-control"
|
||||
placeholder="Password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-footer">
|
||||
<button className="btn btn-primary w-100" disabled={loading}>
|
||||
{loading ? 'loading...' : 'Create new account'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div className="text-center text-secondary mt-3">
|
||||
Already have account?
|
||||
<a className="ml-2" onClick={() => router.push('/api/auth/signin')}>
|
||||
Sign in</a>
|
||||
</div> */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
238
site/components/Subscription.tsx
Normal file
238
site/components/Subscription.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, SetStateAction, Dispatch } from 'react'
|
||||
import Link from 'next/link'
|
||||
import PlanCards from '@/components/Plan'
|
||||
import {
|
||||
UpdateBillingLink,
|
||||
CancelLink,
|
||||
ResumeButton,
|
||||
PauseLink,
|
||||
UnpauseButton
|
||||
} from '@/components/Manage'
|
||||
import { Plan, Subscription, SubscriptionState } from '@/types'
|
||||
|
||||
export const SubscriptionComponent = ({ sub, plans }:
|
||||
{ sub: Subscription | null, plans: Plan[] }
|
||||
) => {
|
||||
|
||||
// Make sure Lemon.js is loaded
|
||||
useEffect(() => {
|
||||
window.createLemonSqueezy()
|
||||
}, [])
|
||||
|
||||
const [subscription, setSubscription] = useState<SubscriptionState>(() => {
|
||||
if (sub) {
|
||||
return {
|
||||
id: sub.lemonSqueezyId,
|
||||
planName: sub.plan?.variantName,
|
||||
planInterval: sub.plan?.interval,
|
||||
productId: sub.plan?.productId,
|
||||
variantId: sub.plan?.variantId,
|
||||
status: sub.status,
|
||||
renewalDate: sub.renewsAt,
|
||||
trialEndDate: sub.trialEndsAt,
|
||||
expiryDate: sub.endsAt,
|
||||
unpauseDate: sub.resumesAt,
|
||||
price: sub.price / 100,
|
||||
}
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
})
|
||||
|
||||
if (sub) {
|
||||
|
||||
switch(subscription?.status) {
|
||||
|
||||
case 'active':
|
||||
return <ActiveSubscription subscription={subscription} setSubscription={setSubscription} />
|
||||
case 'on_trial':
|
||||
return <TrialSubscription subscription={subscription} setSubscription={setSubscription} />
|
||||
case 'past_due':
|
||||
return <PastDueSubscription subscription={subscription} setSubscription={setSubscription} />
|
||||
case 'cancelled':
|
||||
return <CancelledSubscription subscription={subscription} setSubscription={setSubscription} />
|
||||
case 'paused':
|
||||
return <PausedSubscription subscription={subscription} setSubscription={setSubscription} />
|
||||
case 'unpaid':
|
||||
return <UnpaidSubscription subscription={subscription} setSubscription={setSubscription} />
|
||||
case 'expired':
|
||||
return <ExpiredSubscription subscription={subscription} plans={plans} setSubscription={setSubscription} />
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="text-center">Please sign up to a paid plan.</p>
|
||||
|
||||
<PlanCards plans={plans} setSubscription={setSubscription} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const ActiveSubscription = ({ subscription, setSubscription }:
|
||||
{ subscription: SubscriptionState, setSubscription: Dispatch<SetStateAction<SubscriptionState>> }
|
||||
) => {
|
||||
return (
|
||||
<>
|
||||
<p className="mb-2">
|
||||
You are currently on the <b>{subscription?.planName} {subscription?.planInterval}ly</b> plan, paying ${subscription?.price}/{subscription?.planInterval}.
|
||||
</p>
|
||||
|
||||
<p className="mb-2">Your next renewal will be on {formatDate(subscription?.renewalDate)}.</p>
|
||||
|
||||
<hr className="my-8" />
|
||||
|
||||
<p className="mb-4">
|
||||
<Link href="/billing/change-plan" className="px-6 py-2 font-bold">
|
||||
Change plan →
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<p><UpdateBillingLink subscription={subscription} /></p>
|
||||
|
||||
<p><PauseLink subscription={subscription} setSubscription={setSubscription} /></p>
|
||||
|
||||
<p><CancelLink subscription={subscription} setSubscription={setSubscription} /></p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const CancelledSubscription = ({ subscription, setSubscription }:
|
||||
{ subscription: SubscriptionState, setSubscription: Dispatch<SetStateAction<SubscriptionState>> }
|
||||
) => {
|
||||
return (
|
||||
<>
|
||||
<p className="mb-2">
|
||||
You are currently on the <b>{subscription?.planName} {subscription?.planInterval}ly</b> plan, paying ${subscription?.price}/{subscription?.planInterval}.
|
||||
</p>
|
||||
|
||||
<p className="mb-8">Your subscription has been cancelled and <b>will end on {formatDate(subscription?.expiryDate)}</b>. After this date you will no longer have access to the app.</p>
|
||||
|
||||
<p><ResumeButton subscription={subscription} setSubscription={setSubscription} /></p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const PausedSubscription = ({ subscription, setSubscription }:
|
||||
{ subscription: SubscriptionState, setSubscription: Dispatch<SetStateAction<SubscriptionState>> }
|
||||
) => {
|
||||
return (
|
||||
<>
|
||||
<p className="mb-2">
|
||||
You are currently on the <b>{subscription?.planName} {subscription?.planInterval}ly</b> plan, paying ${subscription?.price}/{subscription?.planInterval}.
|
||||
</p>
|
||||
|
||||
{subscription?.unpauseDate ? (
|
||||
<p className="mb-8">Your subscription payments are currently paused. Your subscription will automatically resume on {formatDate(subscription?.unpauseDate)}.</p>
|
||||
) : (
|
||||
<p className="mb-8">Your subscription payments are currently paused.</p>
|
||||
)}
|
||||
|
||||
<p><UnpauseButton subscription={subscription} setSubscription={setSubscription} /></p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const TrialSubscription = ({ subscription, setSubscription }:
|
||||
{ subscription: SubscriptionState, setSubscription: Dispatch<SetStateAction<SubscriptionState>> }
|
||||
) => {
|
||||
return (
|
||||
<>
|
||||
<p className="mb-2">
|
||||
You are currently on a free trial of the <b>{subscription?.planName} {subscription?.planInterval}ly</b> plan, paying ${subscription?.price}/{subscription?.planInterval}.
|
||||
</p>
|
||||
|
||||
<p className="mb-6">Your trial ends on {formatDate(subscription?.trialEndDate)}. You can cancel your subscription before this date and you won't be charged.</p>
|
||||
|
||||
<hr className="my-8" />
|
||||
|
||||
<p className="mb-4">
|
||||
<Link href="/billing/change-plan" className="px-6 py-2 font-bold">
|
||||
Change plan →
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<p><UpdateBillingLink subscription={subscription} /></p>
|
||||
|
||||
<p><CancelLink subscription={subscription} setSubscription={setSubscription} /></p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const PastDueSubscription = ({ subscription, setSubscription }:
|
||||
{ subscription: SubscriptionState, setSubscription: Dispatch<SetStateAction<SubscriptionState>> }
|
||||
) => {
|
||||
return (
|
||||
<>
|
||||
<div className="my-8 p-4">
|
||||
Your latest payment failed. We will re-try this payment up to four times, after which your subscription will be cancelled.<br />
|
||||
If you need to update your billing details, you can do so below.
|
||||
</div>
|
||||
|
||||
<p className="mb-2">
|
||||
You are currently on the <b>{subscription?.planName} {subscription?.planInterval}ly</b> plan, paying ${subscription?.price}/{subscription?.planInterval}.
|
||||
</p>
|
||||
|
||||
<p className="mb-2">We will attempt a payment on {formatDate(subscription?.renewalDate)}.</p>
|
||||
|
||||
<hr className="my-8" />
|
||||
|
||||
<p><UpdateBillingLink subscription={subscription} /></p>
|
||||
|
||||
<p><CancelLink subscription={subscription} setSubscription={setSubscription} /></p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const UnpaidSubscription = ({ subscription, setSubscription }:
|
||||
{ subscription: SubscriptionState, setSubscription: Dispatch<SetStateAction<SubscriptionState>> }
|
||||
) => {
|
||||
/*
|
||||
Unpaid subscriptions have had four failed recovery payments.
|
||||
If you have dunning enabled in your store settings, customers will be sent emails trying to reactivate their subscription.
|
||||
If you don't have dunning enabled the subscription will remain "unpaid".
|
||||
*/
|
||||
return (
|
||||
<>
|
||||
<p className="mb-2">We haven't been able to make a successful payment and your subscription is currently marked as unpaid.</p>
|
||||
|
||||
<p className="mb-6">Please update your billing information to regain access.</p>
|
||||
|
||||
<p><UpdateBillingLink subscription={subscription} elementType="button" /></p>
|
||||
|
||||
<hr className="my-8" />
|
||||
|
||||
<p><CancelLink subscription={subscription} setSubscription={setSubscription} /></p>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const ExpiredSubscription = ({ subscription, plans, setSubscription }:
|
||||
{ subscription: SubscriptionState, plans: Plan[], setSubscription: Dispatch<SetStateAction<SubscriptionState>> }
|
||||
) => {
|
||||
return (
|
||||
<>
|
||||
<p className="mb-2">Your subscription expired on {formatDate(subscription?.expiryDate)}.</p>
|
||||
|
||||
<p className="mb-2">Please create a new subscription to regain access.</p>
|
||||
|
||||
<hr className="my-8" />
|
||||
|
||||
<PlanCards subscription={subscription} plans={plans} setSubscription={setSubscription} />
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function formatDate(date?: Date | null) {
|
||||
if (!date) return ''
|
||||
return new Date(date).toLocaleString('en-US', {
|
||||
month: 'short',
|
||||
day: "2-digit",
|
||||
year: 'numeric'
|
||||
})
|
||||
}
|
||||
32
site/components/layout/EntryHeader.tsx
Normal file
32
site/components/layout/EntryHeader.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Icon from '@/components/Icon';
|
||||
|
||||
export default function EntryHeader() {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<header className="header">
|
||||
<div className="container">
|
||||
<nav className="row items-center">
|
||||
<div className="col-auto">
|
||||
<div className="d-block">
|
||||
<div className="navbar">
|
||||
<div className="navbar-item">
|
||||
<a
|
||||
className="btn"
|
||||
onClick={() => router.push('/')}
|
||||
>
|
||||
<Icon name="chevron-left"/>
|
||||
Back
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { Fragment, MutableRefObject, PropsWithChildren, RefObject, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Fragment,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Dialog, Popover } from '@headlessui/react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { banner, blogEnabled, componentsRounded, iconsCountRounded, sponsorsUrl, uiGithubUrl } from '@/config/site';
|
||||
import { banner,
|
||||
blogEnabled,
|
||||
iconsCountRounded,
|
||||
sponsorsUrl,
|
||||
uiGithubUrl,
|
||||
} from '@/config/site';
|
||||
import Icon from '@/components/Icon';
|
||||
import GoToTop from '@/components/layout/GoToTop';
|
||||
import Link from '@/components/Link';
|
||||
import NavLink from '@/components/NavLink';
|
||||
import Shape from '@/components/Shape';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { signOut, useSession } from 'next-auth/react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const NavDropdown = ({ title, children, active, footer = false }) => {
|
||||
return (
|
||||
@@ -152,7 +163,7 @@ const NavbarLink = (link, menu) => {
|
||||
|
||||
return (
|
||||
// router.pathname.replace(/^\//, '').startsWith(link.menu)
|
||||
<NavLink href={link.href} className="navbar-link" active={false}>
|
||||
<NavLink href={link.href} className="navbar-link">
|
||||
{link.title}
|
||||
</NavLink>
|
||||
);
|
||||
@@ -190,8 +201,97 @@ const SidebarLink = (link, menu, onClick) => {
|
||||
);
|
||||
};
|
||||
|
||||
const Navbar = ({ menu, opened, onClick, ...props }: { menu?: string; opened?: boolean; onClick?: (event: React.MouseEvent) => void; className?: string }) => {
|
||||
return <div className={clsx('navbar', opened && 'opened', props.className)}>{menuLinks.map((link) => (<Fragment key={link.menu}>{NavbarLink(link, menu)}</Fragment>))}</div>;
|
||||
const NavigationAuth = () => {
|
||||
const { data: session, status } = useSession();
|
||||
const router = useRouter();
|
||||
const image = session?.user?.image;
|
||||
const name = session?.user?.name;
|
||||
const email = session?.user?.email;
|
||||
|
||||
const signIn = () => {
|
||||
if (status === 'loading') return;
|
||||
router.push('/api/auth/signin');
|
||||
};
|
||||
|
||||
return <div className="navbar-item d-flex items-center">
|
||||
{
|
||||
!session &&
|
||||
<a onClick={() => signIn()} className={clsx('btn', { disabled: status === 'loading'})}>
|
||||
Log in
|
||||
</a>
|
||||
}
|
||||
{
|
||||
session &&
|
||||
<Popover className="navbar-dropdown">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Popover.Button className={clsx('navbar-link d-flex items-center lh-1 text-reset p-0')}>
|
||||
{
|
||||
image
|
||||
? <span
|
||||
className="avatar avatar"
|
||||
style={{
|
||||
backgroundImage: `url(${image})`,
|
||||
}}
|
||||
/>
|
||||
: <span className="avatar avatar text-center">
|
||||
{name ? name.toUpperCase().substring(0, 1) : 'T'}
|
||||
</span>
|
||||
}
|
||||
<div className="pl-2">
|
||||
<small className="d-block">{name}</small>
|
||||
{
|
||||
email &&
|
||||
<small className="mt-1 small text-muted">{session.user?.email}</small>
|
||||
}
|
||||
</div>
|
||||
</Popover.Button>
|
||||
<Popover.Panel className="navbar-dropdown-menu">
|
||||
<div className="navbar-dropdown-menu-content">
|
||||
<div onClick={() => router.push('billing')} className="navbar-dropdown-menu-link">
|
||||
<div className="row items-center g-3">
|
||||
<div className="col-auto">
|
||||
<Shape icon='rocket'/>
|
||||
</div>
|
||||
<div className="col">
|
||||
<h5>Billing</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div onClick={() => signOut()} className="navbar-dropdown-menu-link">
|
||||
<div className="row items-center g-3">
|
||||
<div className="col-auto">
|
||||
<Shape icon='logout'/>
|
||||
</div>
|
||||
<div className="col">
|
||||
<h5>Log out</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
}
|
||||
</div>;
|
||||
};
|
||||
|
||||
const Navbar = ({
|
||||
menu,
|
||||
opened,
|
||||
onClick,
|
||||
...props
|
||||
}: {
|
||||
menu?: string;
|
||||
opened?: boolean;
|
||||
onClick?: (event: React.MouseEvent) => void;
|
||||
className?: string
|
||||
}) => {
|
||||
return <div className={clsx('navbar', opened && 'opened', props.className)}>
|
||||
{menuLinks.map((link) => (<Fragment key={link.menu}>{NavbarLink(link, menu)}</Fragment>))}
|
||||
<NavigationAuth/>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const Banner = () => {
|
||||
|
||||
@@ -1,40 +1,81 @@
|
||||
import { Plan } from "@/types";
|
||||
|
||||
export const groupBy = function (xs, key) {
|
||||
return xs.reduce(function (rv, x) {
|
||||
;(rv[x[key]] = rv[x[key]] || []).push(x)
|
||||
return rv
|
||||
}, {})
|
||||
}
|
||||
;(rv[x[key]] = rv[x[key]] || []).push(x);
|
||||
return rv;
|
||||
}, {});
|
||||
};
|
||||
|
||||
export const sortByKeys = function (xs) {
|
||||
return Object.keys(xs)
|
||||
.sort()
|
||||
.reduce((obj, key) => {
|
||||
obj[key] = xs[key]
|
||||
return obj
|
||||
}, {})
|
||||
}
|
||||
obj[key] = xs[key];
|
||||
return obj;
|
||||
}, {});
|
||||
};
|
||||
|
||||
export const toPascalCase = function (text: string) {
|
||||
return text
|
||||
.replace(new RegExp(/[-_]+/, "g"), " ")
|
||||
.replace(new RegExp(/[^\w\s]/, "g"), "")
|
||||
.replace(new RegExp(/[-_]+/, 'g'), ' ')
|
||||
.replace(new RegExp(/[^\w\s]/, 'g'), '')
|
||||
.replace(
|
||||
new RegExp(/\s+(.)(\w+)/, "g"),
|
||||
new RegExp(/\s+(.)(\w+)/, 'g'),
|
||||
($1, $2, $3) => `${$2.toUpperCase() + $3.toLowerCase()}`
|
||||
)
|
||||
.replace(new RegExp(/\s/, "g"), "")
|
||||
.replace(new RegExp(/\w/), (s) => s.toUpperCase())
|
||||
}
|
||||
.replace(new RegExp(/\s/, 'g'), '')
|
||||
.replace(new RegExp(/\w/), (s) => s.toUpperCase());
|
||||
};
|
||||
|
||||
export const baseUrl = {
|
||||
development: "http://localhost:3000",
|
||||
production: "https://tabler-icons.io",
|
||||
}[process.env.NODE_ENV]
|
||||
development: 'http://localhost:3000',
|
||||
production: 'https://tabler-icons.io',
|
||||
}[process.env.NODE_ENV];
|
||||
|
||||
export const getCurrentBrand = (hostname: string) => {
|
||||
if (hostname && hostname.match(/tabler-icons/)) {
|
||||
return "tabler-icons"
|
||||
return 'tabler-icons';
|
||||
}
|
||||
|
||||
return "tabler-ui"
|
||||
}
|
||||
return 'tabler-ui';
|
||||
};
|
||||
|
||||
export const getNextAuthErrorMessage = (error: string): string => {
|
||||
// Nextauth errors
|
||||
// https://next-auth.js.org/configuration/pages#sign-in-page
|
||||
// OAuthSignin: Error in constructing an authorization URL (1, 2, 3),
|
||||
// OAuthCallback: Error in handling the response (1, 2, 3) from an OAuth provider.
|
||||
// OAuthCreateAccount: Could not create OAuth provider user in the database.
|
||||
// EmailCreateAccount: Could not create email provider user in the database.
|
||||
// Callback: Error in the OAuth callback handler route
|
||||
// OAuthAccountNotLinked: If the email on the account is already linked, but not with this OAuth account
|
||||
// EmailSignin: Sending the e-mail with the verification token failed
|
||||
// CredentialsSignin: The authorize callback returned null in the Credentials provider. We don't recommend providing information about which part of the credentials were wrong, as it might be abused by malicious hackers.
|
||||
// SessionRequired: The content of this page requires you to be signed in at all times. See useSession for configuration.
|
||||
// Default: Catch all, will apply, if none of the above matched
|
||||
|
||||
switch (error) {
|
||||
case 'OAuthSignin':
|
||||
case 'OAuthCallback':
|
||||
case 'OAuthCreateAccount':
|
||||
case 'EmailCreateAccount':
|
||||
case 'Callback':
|
||||
return 'Try signing in with a different account.';
|
||||
case 'OAuthAccountNotLinked':
|
||||
return 'To confirm your identity, sign in with the same account you used originally.';
|
||||
case 'EmailSignin':
|
||||
return 'The e-mail could not be sent.';
|
||||
case 'CredentialsSignin':
|
||||
return 'Sign in failed. Check the details you provided are correct.';
|
||||
case 'SessionRequired':
|
||||
return 'Please sign in to access this page.';
|
||||
case 'Default':
|
||||
default:
|
||||
return 'Unable to sign in.';
|
||||
}
|
||||
};
|
||||
|
||||
export const isPlanFeatured = (plan: Plan) => {
|
||||
return plan.variantName === 'Advanced'
|
||||
}
|
||||
112
site/lib/auth.ts
Normal file
112
site/lib/auth.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { PrismaAdapter } from '@next-auth/prisma-adapter'
|
||||
import GitHubProvider from 'next-auth/providers/github'
|
||||
import GoogleProvider from 'next-auth/providers/google'
|
||||
import { NextAuthOptions } from 'next-auth'
|
||||
import Auth0Provider from 'next-auth/providers/auth0'
|
||||
import { getServerSession } from 'next-auth/next'
|
||||
import type { ExtendedSession } from '@/types'
|
||||
|
||||
export const authConfig: NextAuthOptions = {
|
||||
providers: [
|
||||
// CredentialsProvider({
|
||||
// name: 'Login to your account',
|
||||
// credentials: {
|
||||
// email: {
|
||||
// label: 'Email address',
|
||||
// type: 'text',
|
||||
// placeholder: 'your@email.com"'
|
||||
// },
|
||||
// password: {
|
||||
// label: 'Password',
|
||||
// type: 'password',
|
||||
// placeholder: 'Your password'
|
||||
// },
|
||||
// },
|
||||
// async authorize(credentials) {
|
||||
// if (!credentials || !credentials.email || !credentials.password) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// const user = await prisma.user.findUnique({
|
||||
// where: { email: credentials.email }
|
||||
// });
|
||||
|
||||
// if (!user) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// const isPasswordValid = await compare(
|
||||
// credentials.password,
|
||||
// user.password
|
||||
// );
|
||||
|
||||
// if (!isPasswordValid) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// const { password, ...userWithoutPassword } = user;
|
||||
|
||||
// return userWithoutPassword as User;
|
||||
// }
|
||||
// }),
|
||||
GitHubProvider({
|
||||
clientId: process.env.GITHUB_ID as string,
|
||||
clientSecret: process.env.GITHUB_SECRET as string,
|
||||
}),
|
||||
GoogleProvider({
|
||||
clientId: process.env.GOOGLE_ID as string,
|
||||
clientSecret: process.env.GOOGLE_SECRET as string,
|
||||
}),
|
||||
Auth0Provider({
|
||||
clientId: process.env.AUTH0_CLIENT_ID as string,
|
||||
clientSecret: process.env.AUTH0_CLIENT_SECRET as string,
|
||||
issuer: process.env.AUTH0_ISSUER as string
|
||||
}),
|
||||
],
|
||||
pages: {
|
||||
signIn: '/signin'
|
||||
},
|
||||
adapter: PrismaAdapter(prisma),
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
session: {
|
||||
strategy: 'jwt',
|
||||
},
|
||||
callbacks: {
|
||||
// Add user ID to session from token
|
||||
session: async ({ session, token }) => {
|
||||
return {
|
||||
...session,
|
||||
user: {
|
||||
...session.user,
|
||||
id: token.sub,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
// callbacks: {
|
||||
// session: ({ session, token }) => {
|
||||
// return {
|
||||
// ...session,
|
||||
// user: {
|
||||
// ...session.user,
|
||||
// id: token.id,
|
||||
// },
|
||||
// };
|
||||
// },
|
||||
// jwt: ({ token, user }) => {
|
||||
// if (user) {
|
||||
// const u = user as unknown as any;
|
||||
// return {
|
||||
// ...token,
|
||||
// id: u.id,
|
||||
// };
|
||||
// }
|
||||
// return token;
|
||||
// },
|
||||
// },
|
||||
};
|
||||
|
||||
export function getSession(): Promise<ExtendedSession> {
|
||||
return getServerSession(authConfig) as Promise<ExtendedSession>
|
||||
}
|
||||
50
site/lib/data.ts
Normal file
50
site/lib/data.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
export async function getPlans() {
|
||||
// Gets all active plans
|
||||
return await prisma.plan.findMany({
|
||||
where: {
|
||||
NOT: [
|
||||
{ status: 'draft' },
|
||||
{ status: 'pending' }
|
||||
]
|
||||
},
|
||||
include: {
|
||||
subscriptions: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export async function getPlan(variantId: number) {
|
||||
// Gets single active plan by ID
|
||||
return await prisma.plan.findFirst({
|
||||
where: {
|
||||
variantId: variantId,
|
||||
NOT: [
|
||||
{ status: 'draft' },
|
||||
{ status: 'pending' }
|
||||
]
|
||||
},
|
||||
include: {
|
||||
subscriptions: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export async function getSubscription(userId?: string) {
|
||||
// Gets the most recent subscription
|
||||
return await prisma.subscription.findFirst({
|
||||
where: {
|
||||
userId: userId
|
||||
},
|
||||
include: {
|
||||
plan: true,
|
||||
user: true
|
||||
},
|
||||
orderBy: {
|
||||
lemonSqueezyId: 'desc'
|
||||
}
|
||||
})
|
||||
}
|
||||
14
site/lib/prisma.ts
Normal file
14
site/lib/prisma.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
let prisma: PrismaClient;
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
prisma = new PrismaClient();
|
||||
} else {
|
||||
if (!global.prisma) {
|
||||
global.prisma = new PrismaClient();
|
||||
}
|
||||
prisma = global.prisma;
|
||||
}
|
||||
|
||||
export default prisma;
|
||||
3
site/middleware.ts
Normal file
3
site/middleware.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default } from 'next-auth/middleware';
|
||||
|
||||
export const config = { matcher: ['/billing'] };
|
||||
@@ -10,6 +10,8 @@ const nextConfig = {
|
||||
domains: ["avatars.githubusercontent.com"],
|
||||
},
|
||||
experimental: {
|
||||
appDir: true,
|
||||
serverActions: true,
|
||||
},
|
||||
async redirects() {
|
||||
return JSON.parse(fs.readFileSync('./redirects.json'))
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"private": true,
|
||||
"sourceType": "module",
|
||||
"scripts": {
|
||||
"postinstall": "pnpm prisma generate",
|
||||
"dev": "concurrently \"contentlayer dev\" \"next dev --port 3010\"",
|
||||
"build": "contentlayer build && next build",
|
||||
"start": "next start",
|
||||
@@ -23,10 +24,13 @@
|
||||
"dependencies": {
|
||||
"@glidejs/glide": "^3.6.0",
|
||||
"@headlessui/react": "^1.7.16",
|
||||
"@lemonsqueezy/lemonsqueezy.js": "^1.2.2",
|
||||
"@mdx-js/loader": "^2.3.0",
|
||||
"@mdx-js/react": "2.3.0",
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@next/env": "^13.5.3",
|
||||
"@next/mdx": "^13.5.3",
|
||||
"@prisma/client": "5.1.1",
|
||||
"@sindresorhus/slugify": "^2.2.1",
|
||||
"@svgr/webpack": "^8.0.1",
|
||||
"@tabler/icons": "^2.30.0",
|
||||
@@ -74,7 +78,7 @@
|
||||
"minimatch": "^9.0.3",
|
||||
"modern-async": "^1.1.3",
|
||||
"next": "^13.5.3",
|
||||
"next-auth": "^4.22.3",
|
||||
"next-auth": "^4.23.2",
|
||||
"next-contentlayer": "^0.3.4",
|
||||
"next-mdx-remote": "^4.4.1",
|
||||
"next-sitemap": "^4.1.8",
|
||||
@@ -112,10 +116,13 @@
|
||||
"typescript": "5.1.6",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"webpack": "^5.88.2",
|
||||
"yaml": "^2.3.2"
|
||||
"yaml": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-transform-private-methods": "^7.22.5",
|
||||
"@t3-oss/env-nextjs": "^0.6.0",
|
||||
"prisma": "^5.1.1",
|
||||
"yaml": "^2.3.2",
|
||||
"zod": "^3.21.4"
|
||||
}
|
||||
}
|
||||
|
||||
366
site/pnpm-lock.yaml
generated
366
site/pnpm-lock.yaml
generated
@@ -11,18 +11,27 @@ dependencies:
|
||||
'@headlessui/react':
|
||||
specifier: ^1.7.16
|
||||
version: 1.7.16(react-dom@18.2.0)(react@18.2.0)
|
||||
'@lemonsqueezy/lemonsqueezy.js':
|
||||
specifier: ^1.2.2
|
||||
version: 1.2.2
|
||||
'@mdx-js/loader':
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0(webpack@5.88.2)
|
||||
'@mdx-js/react':
|
||||
specifier: 2.3.0
|
||||
version: 2.3.0(react@18.2.0)
|
||||
'@next-auth/prisma-adapter':
|
||||
specifier: ^1.0.7
|
||||
version: 1.0.7(@prisma/client@5.1.1)(next-auth@4.23.2)
|
||||
'@next/env':
|
||||
specifier: ^13.5.3
|
||||
version: 13.5.3
|
||||
'@next/mdx':
|
||||
specifier: ^13.5.3
|
||||
version: 13.5.3(@mdx-js/loader@2.3.0)(@mdx-js/react@2.3.0)
|
||||
'@prisma/client':
|
||||
specifier: 5.1.1
|
||||
version: 5.1.1(prisma@5.1.1)
|
||||
'@sindresorhus/slugify':
|
||||
specifier: ^2.2.1
|
||||
version: 2.2.1
|
||||
@@ -163,10 +172,10 @@ dependencies:
|
||||
version: 1.1.3
|
||||
next:
|
||||
specifier: ^13.5.3
|
||||
version: 13.5.3(@babel/core@7.21.4)(@opentelemetry/api@1.4.1)(react-dom@18.2.0)(react@18.2.0)(sass@1.64.1)
|
||||
version: 13.5.3(@babel/core@7.22.1)(@opentelemetry/api@1.4.1)(react-dom@18.2.0)(react@18.2.0)(sass@1.64.1)
|
||||
next-auth:
|
||||
specifier: ^4.22.3
|
||||
version: 4.22.3(next@13.5.3)(react-dom@18.2.0)(react@18.2.0)
|
||||
specifier: ^4.23.2
|
||||
version: 4.23.2(next@13.5.3)(react-dom@18.2.0)(react@18.2.0)
|
||||
next-contentlayer:
|
||||
specifier: ^0.3.4
|
||||
version: 0.3.4(contentlayer@0.3.4)(esbuild@0.17.18)(markdown-wasm@1.2.0)(next@13.5.3)(react-dom@18.2.0)(react@18.2.0)
|
||||
@@ -279,13 +288,19 @@ dependencies:
|
||||
specifier: ^5.88.2
|
||||
version: 5.88.2(esbuild@0.17.18)
|
||||
yaml:
|
||||
specifier: ^2.3.2
|
||||
version: 2.3.2
|
||||
specifier: ^2.3.1
|
||||
version: 2.3.1
|
||||
|
||||
devDependencies:
|
||||
'@babel/plugin-transform-private-methods':
|
||||
specifier: ^7.22.5
|
||||
version: 7.22.5(@babel/core@7.22.1)
|
||||
'@t3-oss/env-nextjs':
|
||||
specifier: ^0.6.0
|
||||
version: 0.6.0(typescript@5.1.6)(zod@3.21.4)
|
||||
prisma:
|
||||
specifier: ^5.1.1
|
||||
version: 5.1.1
|
||||
zod:
|
||||
specifier: ^3.21.4
|
||||
version: 3.21.4
|
||||
@@ -298,14 +313,20 @@ packages:
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.1.1
|
||||
'@jridgewell/trace-mapping': 0.3.17
|
||||
dev: false
|
||||
|
||||
/@babel/code-frame@7.21.4:
|
||||
resolution: {integrity: sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/highlight': 7.18.6
|
||||
dev: false
|
||||
|
||||
/@babel/code-frame@7.22.13:
|
||||
resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/highlight': 7.22.20
|
||||
chalk: 2.4.2
|
||||
dev: true
|
||||
|
||||
/@babel/compat-data@7.21.4:
|
||||
resolution: {integrity: sha512-/DYyDpeCfaVinT40FPGdkkb+lYSKvsVuMjDAG7jPOWWiM1ibOaB9CXJAlc4d1QpP/U2q2P9jbrSlClKSErd55g==}
|
||||
@@ -315,7 +336,6 @@ packages:
|
||||
/@babel/compat-data@7.22.3:
|
||||
resolution: {integrity: sha512-aNtko9OPOwVESUFp3MZfD8Uzxl7JzSeJpd7npIoxCasU37PFbAQRpKglkaKwlHOyeJdrREpo8TW8ldrkYWwvIQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: false
|
||||
|
||||
/@babel/core@7.21.4:
|
||||
resolution: {integrity: sha512-qt/YV149Jman/6AfmlxJ04LMIu8bMoyl3RB91yTFrxQmgbrSvQMy7cI8Q62FHx1t8wJ8B5fu0UDoLwHAhUo1QA==}
|
||||
@@ -361,7 +381,6 @@ packages:
|
||||
semver: 6.3.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@babel/generator@7.22.3:
|
||||
resolution: {integrity: sha512-C17MW4wlk//ES/CJDL51kPNwl+qiBQyN7b9SKyVp11BLGFeSPoVaHrv+MNt8jwQFhQWowW88z1eeBx3pFz9v8A==}
|
||||
@@ -371,7 +390,6 @@ packages:
|
||||
'@jridgewell/gen-mapping': 0.3.2
|
||||
'@jridgewell/trace-mapping': 0.3.17
|
||||
jsesc: 2.5.2
|
||||
dev: false
|
||||
|
||||
/@babel/helper-annotate-as-pure@7.18.6:
|
||||
resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==}
|
||||
@@ -380,6 +398,13 @@ packages:
|
||||
'@babel/types': 7.22.3
|
||||
dev: false
|
||||
|
||||
/@babel/helper-annotate-as-pure@7.22.5:
|
||||
resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.23.0
|
||||
dev: true
|
||||
|
||||
/@babel/helper-builder-binary-assignment-operator-visitor@7.18.9:
|
||||
resolution: {integrity: sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -413,7 +438,7 @@ packages:
|
||||
'@babel/helper-validator-option': 7.21.0
|
||||
browserslist: 4.21.5
|
||||
lru-cache: 5.1.1
|
||||
semver: 6.3.0
|
||||
semver: 6.3.1
|
||||
dev: false
|
||||
|
||||
/@babel/helper-compilation-targets@7.22.1(@babel/core@7.22.1):
|
||||
@@ -427,8 +452,7 @@ packages:
|
||||
'@babel/helper-validator-option': 7.21.0
|
||||
browserslist: 4.21.5
|
||||
lru-cache: 5.1.1
|
||||
semver: 6.3.0
|
||||
dev: false
|
||||
semver: 6.3.1
|
||||
|
||||
/@babel/helper-create-class-features-plugin@7.21.4(@babel/core@7.21.4):
|
||||
resolution: {integrity: sha512-46QrX2CQlaFRF4TkwfTt6nJD7IHq8539cCL7SDpqWSDeJKY1xylKKY5F/33mJhLZ3mFvKv2gGrVS6NkyF6qs+Q==}
|
||||
@@ -449,6 +473,24 @@ packages:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@babel/helper-create-class-features-plugin@7.22.15(@babel/core@7.22.1):
|
||||
resolution: {integrity: sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0
|
||||
dependencies:
|
||||
'@babel/core': 7.22.1
|
||||
'@babel/helper-annotate-as-pure': 7.22.5
|
||||
'@babel/helper-environment-visitor': 7.22.20
|
||||
'@babel/helper-function-name': 7.23.0
|
||||
'@babel/helper-member-expression-to-functions': 7.23.0
|
||||
'@babel/helper-optimise-call-expression': 7.22.5
|
||||
'@babel/helper-replace-supers': 7.22.20(@babel/core@7.22.1)
|
||||
'@babel/helper-skip-transparent-expression-wrappers': 7.22.5
|
||||
'@babel/helper-split-export-declaration': 7.22.6
|
||||
semver: 6.3.1
|
||||
dev: true
|
||||
|
||||
/@babel/helper-create-regexp-features-plugin@7.20.5(@babel/core@7.21.4):
|
||||
resolution: {integrity: sha512-m68B1lkg3XDGX5yCvGO0kPx3v9WIYLnzjKfPcQiwntEQa5ZeRkPmo2X/ISJc8qxWGfwUr+kvZAeEzAwLec2r2w==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -471,7 +513,7 @@ packages:
|
||||
debug: 4.3.4
|
||||
lodash.debounce: 4.0.8
|
||||
resolve: 1.22.1
|
||||
semver: 6.3.0
|
||||
semver: 6.3.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
@@ -484,7 +526,11 @@ packages:
|
||||
/@babel/helper-environment-visitor@7.22.1:
|
||||
resolution: {integrity: sha512-Z2tgopurB/kTbidvzeBrc2To3PUP/9i5MUe+fU6QJCQDyPwSH2oRapkLw3KGECDYSjhQZCNxEvNvZlLw8JjGwA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: false
|
||||
|
||||
/@babel/helper-environment-visitor@7.22.20:
|
||||
resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: true
|
||||
|
||||
/@babel/helper-explode-assignable-expression@7.18.6:
|
||||
resolution: {integrity: sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==}
|
||||
@@ -499,14 +545,20 @@ packages:
|
||||
dependencies:
|
||||
'@babel/template': 7.20.7
|
||||
'@babel/types': 7.22.3
|
||||
dev: false
|
||||
|
||||
/@babel/helper-function-name@7.23.0:
|
||||
resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/template': 7.22.15
|
||||
'@babel/types': 7.23.0
|
||||
dev: true
|
||||
|
||||
/@babel/helper-hoist-variables@7.18.6:
|
||||
resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.22.3
|
||||
dev: false
|
||||
|
||||
/@babel/helper-member-expression-to-functions@7.21.0:
|
||||
resolution: {integrity: sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q==}
|
||||
@@ -515,6 +567,13 @@ packages:
|
||||
'@babel/types': 7.22.3
|
||||
dev: false
|
||||
|
||||
/@babel/helper-member-expression-to-functions@7.23.0:
|
||||
resolution: {integrity: sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.23.0
|
||||
dev: true
|
||||
|
||||
/@babel/helper-module-imports@7.18.6:
|
||||
resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -527,7 +586,6 @@ packages:
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.22.3
|
||||
dev: false
|
||||
|
||||
/@babel/helper-module-transforms@7.21.2:
|
||||
resolution: {integrity: sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ==}
|
||||
@@ -559,7 +617,6 @@ packages:
|
||||
'@babel/types': 7.22.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@babel/helper-optimise-call-expression@7.18.6:
|
||||
resolution: {integrity: sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==}
|
||||
@@ -568,11 +625,23 @@ packages:
|
||||
'@babel/types': 7.22.3
|
||||
dev: false
|
||||
|
||||
/@babel/helper-optimise-call-expression@7.22.5:
|
||||
resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.23.0
|
||||
dev: true
|
||||
|
||||
/@babel/helper-plugin-utils@7.20.2:
|
||||
resolution: {integrity: sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: false
|
||||
|
||||
/@babel/helper-plugin-utils@7.22.5:
|
||||
resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: true
|
||||
|
||||
/@babel/helper-remap-async-to-generator@7.18.9(@babel/core@7.21.4):
|
||||
resolution: {integrity: sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -581,7 +650,7 @@ packages:
|
||||
dependencies:
|
||||
'@babel/core': 7.21.4
|
||||
'@babel/helper-annotate-as-pure': 7.18.6
|
||||
'@babel/helper-environment-visitor': 7.18.9
|
||||
'@babel/helper-environment-visitor': 7.22.1
|
||||
'@babel/helper-wrap-function': 7.20.5
|
||||
'@babel/types': 7.22.3
|
||||
transitivePeerDependencies:
|
||||
@@ -592,7 +661,7 @@ packages:
|
||||
resolution: {integrity: sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/helper-environment-visitor': 7.18.9
|
||||
'@babel/helper-environment-visitor': 7.22.1
|
||||
'@babel/helper-member-expression-to-functions': 7.21.0
|
||||
'@babel/helper-optimise-call-expression': 7.18.6
|
||||
'@babel/template': 7.20.7
|
||||
@@ -602,6 +671,18 @@ packages:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@babel/helper-replace-supers@7.22.20(@babel/core@7.22.1):
|
||||
resolution: {integrity: sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0
|
||||
dependencies:
|
||||
'@babel/core': 7.22.1
|
||||
'@babel/helper-environment-visitor': 7.22.20
|
||||
'@babel/helper-member-expression-to-functions': 7.23.0
|
||||
'@babel/helper-optimise-call-expression': 7.22.5
|
||||
dev: true
|
||||
|
||||
/@babel/helper-simple-access@7.20.2:
|
||||
resolution: {integrity: sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -614,7 +695,6 @@ packages:
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.22.3
|
||||
dev: false
|
||||
|
||||
/@babel/helper-skip-transparent-expression-wrappers@7.20.0:
|
||||
resolution: {integrity: sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==}
|
||||
@@ -623,27 +703,47 @@ packages:
|
||||
'@babel/types': 7.22.3
|
||||
dev: false
|
||||
|
||||
/@babel/helper-skip-transparent-expression-wrappers@7.22.5:
|
||||
resolution: {integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.23.0
|
||||
dev: true
|
||||
|
||||
/@babel/helper-split-export-declaration@7.18.6:
|
||||
resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.22.3
|
||||
dev: false
|
||||
|
||||
/@babel/helper-split-export-declaration@7.22.6:
|
||||
resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.23.0
|
||||
dev: true
|
||||
|
||||
/@babel/helper-string-parser@7.21.5:
|
||||
resolution: {integrity: sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: false
|
||||
|
||||
/@babel/helper-string-parser@7.22.5:
|
||||
resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: true
|
||||
|
||||
/@babel/helper-validator-identifier@7.19.1:
|
||||
resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: false
|
||||
|
||||
/@babel/helper-validator-identifier@7.22.20:
|
||||
resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: true
|
||||
|
||||
/@babel/helper-validator-option@7.21.0:
|
||||
resolution: {integrity: sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: false
|
||||
|
||||
/@babel/helper-wrap-function@7.20.5:
|
||||
resolution: {integrity: sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==}
|
||||
@@ -677,7 +777,6 @@ packages:
|
||||
'@babel/types': 7.22.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@babel/highlight@7.18.6:
|
||||
resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==}
|
||||
@@ -686,7 +785,15 @@ packages:
|
||||
'@babel/helper-validator-identifier': 7.19.1
|
||||
chalk: 2.4.2
|
||||
js-tokens: 4.0.0
|
||||
dev: false
|
||||
|
||||
/@babel/highlight@7.22.20:
|
||||
resolution: {integrity: sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.22.20
|
||||
chalk: 2.4.2
|
||||
js-tokens: 4.0.0
|
||||
dev: true
|
||||
|
||||
/@babel/parser@7.22.3:
|
||||
resolution: {integrity: sha512-vrukxyW/ep8UD1UDzOYpTKQ6abgjFoeG6L+4ar9+c5TN9QnlqiOi6QK7LSR5ewm/ERyGkT/Ai6VboNrxhbr9Uw==}
|
||||
@@ -694,7 +801,14 @@ packages:
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@babel/types': 7.22.3
|
||||
dev: false
|
||||
|
||||
/@babel/parser@7.23.0:
|
||||
resolution: {integrity: sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@babel/types': 7.23.0
|
||||
dev: true
|
||||
|
||||
/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.18.6(@babel/core@7.21.4):
|
||||
resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==}
|
||||
@@ -1319,6 +1433,17 @@ packages:
|
||||
'@babel/helper-plugin-utils': 7.20.2
|
||||
dev: false
|
||||
|
||||
/@babel/plugin-transform-private-methods@7.22.5(@babel/core@7.22.1):
|
||||
resolution: {integrity: sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
dependencies:
|
||||
'@babel/core': 7.22.1
|
||||
'@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.22.1)
|
||||
'@babel/helper-plugin-utils': 7.22.5
|
||||
dev: true
|
||||
|
||||
/@babel/plugin-transform-property-literals@7.18.6(@babel/core@7.21.4):
|
||||
resolution: {integrity: sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -1643,7 +1768,6 @@ packages:
|
||||
'@babel/code-frame': 7.21.4
|
||||
'@babel/parser': 7.22.3
|
||||
'@babel/types': 7.22.3
|
||||
dev: false
|
||||
|
||||
/@babel/template@7.21.9:
|
||||
resolution: {integrity: sha512-MK0X5k8NKOuWRamiEfc3KEJiHMTkGZNUjzMipqCGDDc6ijRl/B7RGSKVGncu4Ro/HdyzzY6cmoXuKI2Gffk7vQ==}
|
||||
@@ -1652,7 +1776,15 @@ packages:
|
||||
'@babel/code-frame': 7.21.4
|
||||
'@babel/parser': 7.22.3
|
||||
'@babel/types': 7.22.3
|
||||
dev: false
|
||||
|
||||
/@babel/template@7.22.15:
|
||||
resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.22.13
|
||||
'@babel/parser': 7.23.0
|
||||
'@babel/types': 7.23.0
|
||||
dev: true
|
||||
|
||||
/@babel/traverse@7.22.1:
|
||||
resolution: {integrity: sha512-lAWkdCoUFnmwLBhIRLciFntGYsIIoC6vIbN8zrLPqBnJmPu7Z6nzqnKd7FsxQUNAvZfVZ0x6KdNvNp8zWIOHSQ==}
|
||||
@@ -1670,7 +1802,6 @@ packages:
|
||||
globals: 11.12.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@babel/types@7.22.3:
|
||||
resolution: {integrity: sha512-P3na3xIQHTKY4L0YOG7pM8M8uoUIB910WQaSiiMCZUC2Cy8XFEQONGABFnHWBa2gpGKODTAJcNhi5Zk0sLRrzg==}
|
||||
@@ -1679,7 +1810,15 @@ packages:
|
||||
'@babel/helper-string-parser': 7.21.5
|
||||
'@babel/helper-validator-identifier': 7.19.1
|
||||
to-fast-properties: 2.0.0
|
||||
dev: false
|
||||
|
||||
/@babel/types@7.23.0:
|
||||
resolution: {integrity: sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 7.22.5
|
||||
'@babel/helper-validator-identifier': 7.22.20
|
||||
to-fast-properties: 2.0.0
|
||||
dev: true
|
||||
|
||||
/@contentlayer/cli@0.3.4(esbuild@0.17.18)(markdown-wasm@1.2.0):
|
||||
resolution: {integrity: sha512-vNDwgLuhYNu+m70NZ3XK9kexKNguuxPXg7Yvzj3B34cEilQjjzSrcTY/i+AIQm9V7uT5GGshx9ukzPf+SmoszQ==}
|
||||
@@ -1748,7 +1887,7 @@ packages:
|
||||
micromatch: 4.0.5
|
||||
ts-pattern: 4.3.0
|
||||
unified: 10.1.2
|
||||
yaml: 2.3.2
|
||||
yaml: 2.3.1
|
||||
zod: 3.21.4
|
||||
transitivePeerDependencies:
|
||||
- '@effect-ts/otel-node'
|
||||
@@ -2160,7 +2299,6 @@ packages:
|
||||
dependencies:
|
||||
'@jridgewell/set-array': 1.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.4.14
|
||||
dev: false
|
||||
|
||||
/@jridgewell/gen-mapping@0.3.2:
|
||||
resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==}
|
||||
@@ -2169,17 +2307,14 @@ packages:
|
||||
'@jridgewell/set-array': 1.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.4.14
|
||||
'@jridgewell/trace-mapping': 0.3.17
|
||||
dev: false
|
||||
|
||||
/@jridgewell/resolve-uri@3.1.0:
|
||||
resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
dev: false
|
||||
|
||||
/@jridgewell/set-array@1.1.2:
|
||||
resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
dev: false
|
||||
|
||||
/@jridgewell/source-map@0.3.2:
|
||||
resolution: {integrity: sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==}
|
||||
@@ -2190,14 +2325,12 @@ packages:
|
||||
|
||||
/@jridgewell/sourcemap-codec@1.4.14:
|
||||
resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==}
|
||||
dev: false
|
||||
|
||||
/@jridgewell/trace-mapping@0.3.17:
|
||||
resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==}
|
||||
dependencies:
|
||||
'@jridgewell/resolve-uri': 3.1.0
|
||||
'@jridgewell/sourcemap-codec': 1.4.14
|
||||
dev: false
|
||||
|
||||
/@js-temporal/polyfill@0.4.4:
|
||||
resolution: {integrity: sha512-2X6bvghJ/JAoZO52lbgyAPFj8uCflhTo2g7nkFzEQdXd/D8rEeD4HtmTEpmtGCva260fcd66YNXBOYdnmHqSOg==}
|
||||
@@ -2207,6 +2340,10 @@ packages:
|
||||
tslib: 2.4.1
|
||||
dev: false
|
||||
|
||||
/@lemonsqueezy/lemonsqueezy.js@1.2.2:
|
||||
resolution: {integrity: sha512-6laXtCHYv89boG1TGM0fZtWrp3B8XWIDIeCWVyj37XO4ECyR1EeVIgMQlPbxixuuf7IL+UOGjkQX60Gi8DJKNQ==}
|
||||
dev: false
|
||||
|
||||
/@mdx-js/esbuild@2.3.0(esbuild@0.17.18):
|
||||
resolution: {integrity: sha512-r/vsqsM0E+U4Wr0DK+0EfmABE/eg+8ITW4DjvYdh3ve/tK2safaqHArNnaqbOk1DjYGrhxtoXoGaM3BY8fGBTA==}
|
||||
peerDependencies:
|
||||
@@ -2266,6 +2403,16 @@ packages:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@next-auth/prisma-adapter@1.0.7(@prisma/client@5.1.1)(next-auth@4.23.2):
|
||||
resolution: {integrity: sha512-Cdko4KfcmKjsyHFrWwZ//lfLUbcLqlyFqjd/nYE2m3aZ7tjMNUjpks47iw7NTCnXf+5UWz5Ypyt1dSs1EP5QJw==}
|
||||
peerDependencies:
|
||||
'@prisma/client': '>=2.26.0 || >=3'
|
||||
next-auth: ^4
|
||||
dependencies:
|
||||
'@prisma/client': 5.1.1(prisma@5.1.1)
|
||||
next-auth: 4.23.2(next@13.5.3)(react-dom@18.2.0)(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@next/env@13.5.3:
|
||||
resolution: {integrity: sha512-X4te86vsbjsB7iO4usY9jLPtZ827Mbx+WcwNBGUOIuswuTAKQtzsuoxc/6KLxCMvogKG795MhrR1LDhYgDvasg==}
|
||||
dev: false
|
||||
@@ -2624,6 +2771,28 @@ packages:
|
||||
tslib: 2.4.1
|
||||
dev: false
|
||||
|
||||
/@prisma/client@5.1.1(prisma@5.1.1):
|
||||
resolution: {integrity: sha512-fxcCeK5pMQGcgCqCrWsi+I2rpIbk0rAhdrN+ke7f34tIrgPwA68ensrpin+9+fZvuV2OtzHmuipwduSY6HswdA==}
|
||||
engines: {node: '>=16.13'}
|
||||
requiresBuild: true
|
||||
peerDependencies:
|
||||
prisma: '*'
|
||||
peerDependenciesMeta:
|
||||
prisma:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@prisma/engines-version': 5.1.1-1.6a3747c37ff169c90047725a05a6ef02e32ac97e
|
||||
prisma: 5.1.1
|
||||
dev: false
|
||||
|
||||
/@prisma/engines-version@5.1.1-1.6a3747c37ff169c90047725a05a6ef02e32ac97e:
|
||||
resolution: {integrity: sha512-owZqbY/wucbr65bXJ/ljrHPgQU5xXTSkmcE/JcbqE1kusuAXV/TLN3/exmz21SZ5rJ7WDkyk70J2G/n68iogbQ==}
|
||||
dev: false
|
||||
|
||||
/@prisma/engines@5.1.1:
|
||||
resolution: {integrity: sha512-NV/4nVNWFZSJCCIA3HIFJbbDKO/NARc9ej0tX5S9k2EVbkrFJC4Xt9b0u4rNZWL4V+F5LAjvta8vzEUw0rw+HA==}
|
||||
requiresBuild: true
|
||||
|
||||
/@protobufjs/aspromise@1.1.2:
|
||||
resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==}
|
||||
dev: false
|
||||
@@ -2672,8 +2841,8 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
dev: false
|
||||
|
||||
/@rushstack/eslint-patch@1.4.0:
|
||||
resolution: {integrity: sha512-cEjvTPU32OM9lUFegJagO0mRnIn+rbqrG89vV8/xLnLFX0DoR0r1oy5IlTga71Q7uT3Qus7qm7wgeiMT/+Irlg==}
|
||||
/@rushstack/eslint-patch@1.5.1:
|
||||
resolution: {integrity: sha512-6i/8UoL0P5y4leBIGzvkZdS85RDMG9y1ihZzmTZQ5LdHUYmZ7pKFoj8X0236s3lusPs1Fa5HTQUpwI+UfTcmeA==}
|
||||
dev: false
|
||||
|
||||
/@shuding/opentype.js@1.4.0-beta.0:
|
||||
@@ -3295,7 +3464,6 @@ packages:
|
||||
engines: {node: '>=4'}
|
||||
dependencies:
|
||||
color-convert: 1.9.3
|
||||
dev: false
|
||||
|
||||
/ansi-styles@4.3.0:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
@@ -3448,8 +3616,8 @@ packages:
|
||||
peerDependencies:
|
||||
postcss: ^8.1.0
|
||||
dependencies:
|
||||
browserslist: 4.21.11
|
||||
caniuse-lite: 1.0.30001539
|
||||
browserslist: 4.22.1
|
||||
caniuse-lite: 1.0.30001546
|
||||
fraction.js: 4.3.6
|
||||
normalize-range: 0.1.2
|
||||
picocolors: 1.0.0
|
||||
@@ -3559,17 +3727,6 @@ packages:
|
||||
fill-range: 7.0.1
|
||||
dev: false
|
||||
|
||||
/browserslist@4.21.11:
|
||||
resolution: {integrity: sha512-xn1UXOKUz7DjdGlg9RrUr0GGiWzI97UQJnugHtH0OLDfJB7jMgoIkYvRIEO1l9EeEERVqeqLYOcFBW9ldjypbQ==}
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
caniuse-lite: 1.0.30001539
|
||||
electron-to-chromium: 1.4.528
|
||||
node-releases: 2.0.13
|
||||
update-browserslist-db: 1.0.13(browserslist@4.21.11)
|
||||
dev: false
|
||||
|
||||
/browserslist@4.21.5:
|
||||
resolution: {integrity: sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==}
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
@@ -3579,6 +3736,16 @@ packages:
|
||||
electron-to-chromium: 1.4.284
|
||||
node-releases: 2.0.8
|
||||
update-browserslist-db: 1.0.10(browserslist@4.21.5)
|
||||
|
||||
/browserslist@4.22.1:
|
||||
resolution: {integrity: sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==}
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
caniuse-lite: 1.0.30001546
|
||||
electron-to-chromium: 1.4.544
|
||||
node-releases: 2.0.13
|
||||
update-browserslist-db: 1.0.13(browserslist@4.22.1)
|
||||
dev: false
|
||||
|
||||
/buffer-from@1.1.2:
|
||||
@@ -3635,10 +3802,9 @@ packages:
|
||||
|
||||
/caniuse-lite@1.0.30001464:
|
||||
resolution: {integrity: sha512-oww27MtUmusatpRpCGSOneQk2/l5czXANDSFvsc7VuOQ86s3ANhZetpwXNf1zY/zdfP63Xvjz325DAdAoES13g==}
|
||||
dev: false
|
||||
|
||||
/caniuse-lite@1.0.30001539:
|
||||
resolution: {integrity: sha512-hfS5tE8bnNiNvEOEkm8HElUHroYwlqMMENEzELymy77+tJ6m+gA2krtHl5hxJaj71OlpC2cHZbdSMX1/YEqEkA==}
|
||||
/caniuse-lite@1.0.30001546:
|
||||
resolution: {integrity: sha512-zvtSJwuQFpewSyRrI3AsftF6rM0X80mZkChIt1spBGEvRglCrjTniXvinc8JKRoqTwXAgvqTImaN9igfSMtUBw==}
|
||||
dev: false
|
||||
|
||||
/ccount@2.0.1:
|
||||
@@ -3652,7 +3818,6 @@ packages:
|
||||
ansi-styles: 3.2.1
|
||||
escape-string-regexp: 1.0.5
|
||||
supports-color: 5.5.0
|
||||
dev: false
|
||||
|
||||
/chalk@4.1.2:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
@@ -3751,7 +3916,6 @@ packages:
|
||||
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
||||
dependencies:
|
||||
color-name: 1.1.3
|
||||
dev: false
|
||||
|
||||
/color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
@@ -3762,7 +3926,6 @@ packages:
|
||||
|
||||
/color-name@1.1.3:
|
||||
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
|
||||
dev: false
|
||||
|
||||
/color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
@@ -3859,7 +4022,6 @@ packages:
|
||||
|
||||
/convert-source-map@1.9.0:
|
||||
resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
|
||||
dev: false
|
||||
|
||||
/cookie@0.5.0:
|
||||
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
|
||||
@@ -4011,7 +4173,6 @@ packages:
|
||||
optional: true
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
dev: false
|
||||
|
||||
/decode-named-character-reference@1.0.2:
|
||||
resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==}
|
||||
@@ -4152,10 +4313,9 @@ packages:
|
||||
|
||||
/electron-to-chromium@1.4.284:
|
||||
resolution: {integrity: sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==}
|
||||
dev: false
|
||||
|
||||
/electron-to-chromium@1.4.528:
|
||||
resolution: {integrity: sha512-UdREXMXzLkREF4jA8t89FQjA8WHI6ssP38PMY4/4KhXFQbtImnghh4GkCgrtiZwLKUKVD2iTVXvDVQjfomEQuA==}
|
||||
/electron-to-chromium@1.4.544:
|
||||
resolution: {integrity: sha512-54z7squS1FyFRSUqq/knOFSptjjogLZXbKcYk3B0qkE1KZzvqASwRZnY2KzZQJqIYLVD38XZeoiMRflYSwyO4w==}
|
||||
dev: false
|
||||
|
||||
/emoji-regex@10.2.1:
|
||||
@@ -4304,7 +4464,7 @@ packages:
|
||||
resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
dependencies:
|
||||
get-intrinsic: 1.1.3
|
||||
get-intrinsic: 1.2.1
|
||||
has: 1.0.3
|
||||
has-tostringtag: 1.0.0
|
||||
dev: false
|
||||
@@ -4357,7 +4517,6 @@ packages:
|
||||
/escalade@3.1.1:
|
||||
resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/escape-html@1.0.3:
|
||||
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
||||
@@ -4366,7 +4525,6 @@ packages:
|
||||
/escape-string-regexp@1.0.5:
|
||||
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
dev: false
|
||||
|
||||
/escape-string-regexp@4.0.0:
|
||||
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||
@@ -4388,7 +4546,7 @@ packages:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@next/eslint-plugin-next': 13.5.3
|
||||
'@rushstack/eslint-patch': 1.4.0
|
||||
'@rushstack/eslint-patch': 1.5.1
|
||||
'@typescript-eslint/parser': 5.48.0(eslint@8.31.0)(typescript@5.1.6)
|
||||
eslint: 8.31.0
|
||||
eslint-import-resolver-node: 0.3.6
|
||||
@@ -4956,7 +5114,6 @@ packages:
|
||||
/gensync@1.0.0-beta.2:
|
||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: false
|
||||
|
||||
/get-caller-file@2.0.5:
|
||||
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||
@@ -4990,7 +5147,7 @@ packages:
|
||||
engines: {node: '>= 0.4'}
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
get-intrinsic: 1.1.3
|
||||
get-intrinsic: 1.2.1
|
||||
dev: false
|
||||
|
||||
/get-tsconfig@4.3.0:
|
||||
@@ -5055,7 +5212,6 @@ packages:
|
||||
/globals@11.12.0:
|
||||
resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
|
||||
engines: {node: '>=4'}
|
||||
dev: false
|
||||
|
||||
/globals@13.19.0:
|
||||
resolution: {integrity: sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==}
|
||||
@@ -5105,7 +5261,7 @@ packages:
|
||||
/gopd@1.0.1:
|
||||
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
|
||||
dependencies:
|
||||
get-intrinsic: 1.1.3
|
||||
get-intrinsic: 1.2.1
|
||||
dev: false
|
||||
|
||||
/graceful-fs@4.2.10:
|
||||
@@ -5133,7 +5289,6 @@ packages:
|
||||
/has-flag@3.0.0:
|
||||
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
|
||||
engines: {node: '>=4'}
|
||||
dev: false
|
||||
|
||||
/has-flag@4.0.0:
|
||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||
@@ -5148,7 +5303,7 @@ packages:
|
||||
/has-property-descriptors@1.0.0:
|
||||
resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==}
|
||||
dependencies:
|
||||
get-intrinsic: 1.1.3
|
||||
get-intrinsic: 1.2.1
|
||||
dev: false
|
||||
|
||||
/has-proto@1.0.1:
|
||||
@@ -5816,7 +5971,6 @@ packages:
|
||||
|
||||
/js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
dev: false
|
||||
|
||||
/js-yaml@3.14.1:
|
||||
resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
|
||||
@@ -5846,7 +6000,6 @@ packages:
|
||||
resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==}
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/json-parse-even-better-errors@2.3.1:
|
||||
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
|
||||
@@ -5871,7 +6024,6 @@ packages:
|
||||
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/jsonfile@6.1.0:
|
||||
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
||||
@@ -6035,7 +6187,6 @@ packages:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
dependencies:
|
||||
yallist: 3.1.1
|
||||
dev: false
|
||||
|
||||
/lru-cache@6.0.0:
|
||||
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
|
||||
@@ -7092,7 +7243,6 @@ packages:
|
||||
|
||||
/ms@2.1.2:
|
||||
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
|
||||
dev: false
|
||||
|
||||
/ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
@@ -7122,8 +7272,8 @@ packages:
|
||||
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
||||
dev: false
|
||||
|
||||
/next-auth@4.22.3(next@13.5.3)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-XAgy9xV3J2eJOXrQhmxdjV6MLM29ibm6WtMXc3KY6IPZeApf+SuBuPvlqCUfbu5YsAzlg9WSw6u01dChTfeZOA==}
|
||||
/next-auth@4.23.2(next@13.5.3)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-VRmInu0r/yZNFQheDFeOKtiugu3bt90Po3owAQDnFQ3YLQFmUKgFjcE2+3L0ny5jsJpBXaKbm7j7W2QTc6Ye2A==}
|
||||
peerDependencies:
|
||||
next: ^12.2.5 || ^13
|
||||
nodemailer: ^6.6.5
|
||||
@@ -7137,7 +7287,7 @@ packages:
|
||||
'@panva/hkdf': 1.0.4
|
||||
cookie: 0.5.0
|
||||
jose: 4.13.1
|
||||
next: 13.5.3(@babel/core@7.21.4)(@opentelemetry/api@1.4.1)(react-dom@18.2.0)(react@18.2.0)(sass@1.64.1)
|
||||
next: 13.5.3(@babel/core@7.22.1)(@opentelemetry/api@1.4.1)(react-dom@18.2.0)(react@18.2.0)(sass@1.64.1)
|
||||
oauth: 0.9.15
|
||||
openid-client: 5.4.0
|
||||
preact: 10.13.1
|
||||
@@ -7158,7 +7308,7 @@ packages:
|
||||
'@contentlayer/core': 0.3.4(esbuild@0.17.18)(markdown-wasm@1.2.0)
|
||||
'@contentlayer/utils': 0.3.4
|
||||
contentlayer: 0.3.4(esbuild@0.17.18)(markdown-wasm@1.2.0)
|
||||
next: 13.5.3(@babel/core@7.21.4)(@opentelemetry/api@1.4.1)(react-dom@18.2.0)(react@18.2.0)(sass@1.64.1)
|
||||
next: 13.5.3(@babel/core@7.22.1)(@opentelemetry/api@1.4.1)(react-dom@18.2.0)(react@18.2.0)(sass@1.64.1)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
transitivePeerDependencies:
|
||||
@@ -7196,10 +7346,10 @@ packages:
|
||||
'@next/env': 13.5.3
|
||||
fast-glob: 3.2.12
|
||||
minimist: 1.2.8
|
||||
next: 13.5.3(@babel/core@7.21.4)(@opentelemetry/api@1.4.1)(react-dom@18.2.0)(react@18.2.0)(sass@1.64.1)
|
||||
next: 13.5.3(@babel/core@7.22.1)(@opentelemetry/api@1.4.1)(react-dom@18.2.0)(react@18.2.0)(sass@1.64.1)
|
||||
dev: false
|
||||
|
||||
/next@13.5.3(@babel/core@7.21.4)(@opentelemetry/api@1.4.1)(react-dom@18.2.0)(react@18.2.0)(sass@1.64.1):
|
||||
/next@13.5.3(@babel/core@7.22.1)(@opentelemetry/api@1.4.1)(react-dom@18.2.0)(react@18.2.0)(sass@1.64.1):
|
||||
resolution: {integrity: sha512-4Nt4HRLYDW/yRpJ/QR2t1v63UOMS55A38dnWv3UDOWGezuY0ZyFO1ABNbD7mulVzs9qVhgy2+ppjdsANpKP1mg==}
|
||||
engines: {node: '>=16.14.0'}
|
||||
hasBin: true
|
||||
@@ -7223,7 +7373,7 @@ packages:
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
sass: 1.64.1
|
||||
styled-jsx: 5.1.1(@babel/core@7.21.4)(react@18.2.0)
|
||||
styled-jsx: 5.1.1(@babel/core@7.22.1)(react@18.2.0)
|
||||
watchpack: 2.4.0
|
||||
zod: 3.21.4
|
||||
optionalDependencies:
|
||||
@@ -7249,7 +7399,7 @@ packages:
|
||||
react-dom: '>= 16.0.0'
|
||||
dependencies:
|
||||
'@types/nprogress': 0.2.0
|
||||
next: 13.5.3(@babel/core@7.21.4)(@opentelemetry/api@1.4.1)(react-dom@18.2.0)(react@18.2.0)(sass@1.64.1)
|
||||
next: 13.5.3(@babel/core@7.22.1)(@opentelemetry/api@1.4.1)(react-dom@18.2.0)(react@18.2.0)(sass@1.64.1)
|
||||
nprogress: 0.2.0
|
||||
prop-types: 15.8.1
|
||||
react: 18.2.0
|
||||
@@ -7289,7 +7439,6 @@ packages:
|
||||
|
||||
/node-releases@2.0.8:
|
||||
resolution: {integrity: sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==}
|
||||
dev: false
|
||||
|
||||
/nopt@6.0.0:
|
||||
resolution: {integrity: sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==}
|
||||
@@ -7369,7 +7518,7 @@ packages:
|
||||
engines: {node: '>= 0.4'}
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
define-properties: 1.1.4
|
||||
define-properties: 1.2.1
|
||||
has-symbols: 1.0.3
|
||||
object-keys: 1.1.1
|
||||
dev: false
|
||||
@@ -7626,7 +7775,6 @@ packages:
|
||||
|
||||
/picocolors@1.0.0:
|
||||
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
|
||||
dev: false
|
||||
|
||||
/picomatch@2.3.1:
|
||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||
@@ -7700,6 +7848,14 @@ packages:
|
||||
resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==}
|
||||
dev: false
|
||||
|
||||
/prisma@5.1.1:
|
||||
resolution: {integrity: sha512-WJFG/U7sMmcc6TjJTTifTfpI6Wjoh55xl4AzopVwAdyK68L9/ogNo8QQ2cxuUjJf/Wa82z/uhyh3wMzvRIBphg==}
|
||||
engines: {node: '>=16.13'}
|
||||
hasBin: true
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
'@prisma/engines': 5.1.1
|
||||
|
||||
/prismjs@1.27.0:
|
||||
resolution: {integrity: sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -8098,7 +8254,7 @@ packages:
|
||||
estree-util-value-to-estree: 3.0.1
|
||||
toml: 3.0.0
|
||||
unified: 10.1.2
|
||||
yaml: 2.3.2
|
||||
yaml: 2.3.1
|
||||
dev: false
|
||||
|
||||
/remark-mdx@2.3.0:
|
||||
@@ -8331,7 +8487,7 @@ packages:
|
||||
resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==}
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
get-intrinsic: 1.1.3
|
||||
get-intrinsic: 1.2.1
|
||||
is-regex: 1.1.4
|
||||
dev: false
|
||||
|
||||
@@ -8401,12 +8557,10 @@ packages:
|
||||
/semver@6.3.0:
|
||||
resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/semver@6.3.1:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/semver@7.5.4:
|
||||
resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==}
|
||||
@@ -8658,7 +8812,7 @@ packages:
|
||||
inline-style-parser: 0.1.1
|
||||
dev: false
|
||||
|
||||
/styled-jsx@5.1.1(@babel/core@7.21.4)(react@18.2.0):
|
||||
/styled-jsx@5.1.1(@babel/core@7.22.1)(react@18.2.0):
|
||||
resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
peerDependencies:
|
||||
@@ -8671,7 +8825,7 @@ packages:
|
||||
babel-plugin-macros:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/core': 7.21.4
|
||||
'@babel/core': 7.22.1
|
||||
client-only: 0.0.1
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
@@ -8681,7 +8835,6 @@ packages:
|
||||
engines: {node: '>=4'}
|
||||
dependencies:
|
||||
has-flag: 3.0.0
|
||||
dev: false
|
||||
|
||||
/supports-color@7.2.0:
|
||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||
@@ -8790,7 +8943,6 @@ packages:
|
||||
/to-fast-properties@2.0.0:
|
||||
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
|
||||
engines: {node: '>=4'}
|
||||
dev: false
|
||||
|
||||
/to-regex-range@5.0.1:
|
||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
@@ -8908,7 +9060,7 @@ packages:
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
for-each: 0.3.3
|
||||
is-typed-array: 1.1.10
|
||||
is-typed-array: 1.1.12
|
||||
dev: false
|
||||
|
||||
/typescript@5.1.6:
|
||||
@@ -9109,15 +9261,14 @@ packages:
|
||||
browserslist: 4.21.5
|
||||
escalade: 3.1.1
|
||||
picocolors: 1.0.0
|
||||
dev: false
|
||||
|
||||
/update-browserslist-db@1.0.13(browserslist@4.21.11):
|
||||
/update-browserslist-db@1.0.13(browserslist@4.22.1):
|
||||
resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
browserslist: '>= 4.21.0'
|
||||
dependencies:
|
||||
browserslist: 4.21.11
|
||||
browserslist: 4.22.1
|
||||
escalade: 3.1.1
|
||||
picocolors: 1.0.0
|
||||
dev: false
|
||||
@@ -9349,7 +9500,6 @@ packages:
|
||||
|
||||
/yallist@3.1.1:
|
||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||
dev: false
|
||||
|
||||
/yallist@4.0.0:
|
||||
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
||||
@@ -9360,8 +9510,8 @@ packages:
|
||||
engines: {node: '>= 6'}
|
||||
dev: false
|
||||
|
||||
/yaml@2.3.2:
|
||||
resolution: {integrity: sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==}
|
||||
/yaml@2.3.1:
|
||||
resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==}
|
||||
engines: {node: '>= 14'}
|
||||
dev: false
|
||||
|
||||
|
||||
138
site/prisma/migrations/20231206000400_init/migration.sql
Normal file
138
site/prisma/migrations/20231206000400_init/migration.sql
Normal file
@@ -0,0 +1,138 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "accounts" (
|
||||
"id" TEXT NOT NULL,
|
||||
"user_id" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"provider" TEXT NOT NULL,
|
||||
"provider_account_id" TEXT NOT NULL,
|
||||
"refresh_token" TEXT,
|
||||
"access_token" TEXT,
|
||||
"expires_at" INTEGER,
|
||||
"token_type" TEXT,
|
||||
"scope" TEXT,
|
||||
"id_token" TEXT,
|
||||
"session_state" TEXT,
|
||||
"refresh_token_expires_in" INTEGER,
|
||||
|
||||
CONSTRAINT "accounts_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "sessions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"session_token" TEXT NOT NULL,
|
||||
"user_id" TEXT NOT NULL,
|
||||
"expires" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "sessions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "users" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT,
|
||||
"email" TEXT,
|
||||
"email_verified" TIMESTAMP(3),
|
||||
"image" TEXT,
|
||||
|
||||
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "verificationtokens" (
|
||||
"identifier" TEXT NOT NULL,
|
||||
"token" TEXT NOT NULL,
|
||||
"expires" TIMESTAMP(3) NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Subscription" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"lemon_squeezy_id" INTEGER NOT NULL,
|
||||
"order_id" INTEGER NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL,
|
||||
"renews_at" TIMESTAMP(3),
|
||||
"ends_at" TIMESTAMP(3),
|
||||
"trial_ends_at" TIMESTAMP(3),
|
||||
"resumes_at" TIMESTAMP(3),
|
||||
"price" INTEGER NOT NULL,
|
||||
"plan_id" INTEGER NOT NULL,
|
||||
"user_id" TEXT NOT NULL,
|
||||
"is_usage_based" BOOLEAN NOT NULL DEFAULT false,
|
||||
"subscription_item_id" INTEGER,
|
||||
|
||||
CONSTRAINT "Subscription_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Plan" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"product_id" INTEGER NOT NULL,
|
||||
"variant_id" INTEGER NOT NULL,
|
||||
"name" TEXT,
|
||||
"description" TEXT,
|
||||
"variant_name" TEXT NOT NULL,
|
||||
"sort" INTEGER NOT NULL,
|
||||
"status" TEXT NOT NULL,
|
||||
"price" INTEGER NOT NULL,
|
||||
"interval" TEXT NOT NULL,
|
||||
"interval_count" INTEGER NOT NULL DEFAULT 1,
|
||||
|
||||
CONSTRAINT "Plan_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "WebhookEvent" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"event_name" TEXT NOT NULL,
|
||||
"processed" BOOLEAN NOT NULL DEFAULT false,
|
||||
"body" JSONB NOT NULL,
|
||||
"processing_error" TEXT,
|
||||
|
||||
CONSTRAINT "WebhookEvent_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "accounts_provider_provider_account_id_key" ON "accounts"("provider", "provider_account_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "sessions_session_token_key" ON "sessions"("session_token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "verificationtokens_token_key" ON "verificationtokens"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "verificationtokens_identifier_token_key" ON "verificationtokens"("identifier", "token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Subscription_lemon_squeezy_id_key" ON "Subscription"("lemon_squeezy_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Subscription_order_id_key" ON "Subscription"("order_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Subscription_subscription_item_id_key" ON "Subscription"("subscription_item_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Subscription_plan_id_lemon_squeezy_id_idx" ON "Subscription"("plan_id", "lemon_squeezy_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Plan_variant_id_key" ON "Plan"("variant_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_plan_id_fkey" FOREIGN KEY ("plan_id") REFERENCES "Plan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
3
site/prisma/migrations/migration_lock.toml
Normal file
3
site/prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
||||
107
site/prisma/schema.prisma
Normal file
107
site/prisma/schema.prisma
Normal file
@@ -0,0 +1,107 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("POSTGRES_PRISMA_URL")
|
||||
directUrl = env("POSTGRES_URL_NON_POOLING")
|
||||
}
|
||||
|
||||
model Account {
|
||||
id String @id @default(cuid())
|
||||
userId String @map("user_id")
|
||||
type String
|
||||
provider String
|
||||
providerAccountId String @map("provider_account_id")
|
||||
refresh_token String?
|
||||
access_token String?
|
||||
expires_at Int?
|
||||
token_type String?
|
||||
scope String?
|
||||
id_token String?
|
||||
session_state String?
|
||||
refresh_token_expires_in Int?
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([provider, providerAccountId])
|
||||
@@map("accounts")
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
sessionToken String @unique @map("session_token")
|
||||
userId String @map("user_id")
|
||||
expires DateTime
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("sessions")
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime? @map("email_verified")
|
||||
image String?
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
subscription Subscription[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model VerificationToken {
|
||||
identifier String
|
||||
token String @unique
|
||||
expires DateTime
|
||||
|
||||
@@unique([identifier, token])
|
||||
@@map("verificationtokens")
|
||||
}
|
||||
|
||||
model Subscription {
|
||||
id Int @id @default(autoincrement())
|
||||
lemonSqueezyId Int @unique @map("lemon_squeezy_id")
|
||||
orderId Int @unique @map("order_id")
|
||||
name String
|
||||
email String
|
||||
status String
|
||||
renewsAt DateTime? @map("renews_at")
|
||||
endsAt DateTime? @map("ends_at")
|
||||
trialEndsAt DateTime? @map("trial_ends_at")
|
||||
resumesAt DateTime? @map("resumes_at")
|
||||
price Int
|
||||
plan Plan @relation(fields: [planId], references: [id])
|
||||
planId Int @map("plan_id")
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId String @map("user_id")
|
||||
isUsageBased Boolean @default(false) @map("is_usage_based")
|
||||
subscriptionItemId Int? @unique @map("subscription_item_id")
|
||||
|
||||
@@index([planId, lemonSqueezyId])
|
||||
}
|
||||
|
||||
model Plan {
|
||||
id Int @id @default(autoincrement())
|
||||
productId Int @map("product_id")
|
||||
variantId Int @unique @map("variant_id")
|
||||
name String? // Need to get from Product
|
||||
description String?
|
||||
variantName String @map("variant_name")
|
||||
sort Int
|
||||
status String
|
||||
price Int
|
||||
interval String
|
||||
intervalCount Int @default(1) @map("interval_count")
|
||||
subscriptions Subscription[]
|
||||
}
|
||||
|
||||
model WebhookEvent {
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
eventName String @map("event_name")
|
||||
processed Boolean @default(false)
|
||||
body Json
|
||||
processingError String? @map("processing_error")
|
||||
}
|
||||
@@ -46,6 +46,7 @@
|
||||
|
||||
.icon {
|
||||
width: divide(20, 16) * 1em;
|
||||
min-width: divide(20, 16) * 1em;
|
||||
height: divide(20, 16) * 1em;
|
||||
vertical-align: bottom;
|
||||
margin-right: $gap-2;
|
||||
|
||||
@@ -21,6 +21,10 @@ Cards
|
||||
.card-body {
|
||||
padding: $gap-4;
|
||||
flex: 1;
|
||||
|
||||
.card-md > & {
|
||||
padding: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.card-title {
|
||||
|
||||
@@ -167,6 +167,7 @@ $grid-breakpoints: (
|
||||
$container-max-width: px2rem(1280px);
|
||||
$container-narrow-max-width: px2rem(990px);
|
||||
$container-slim-max-width: px2rem(660px);
|
||||
$container-tight-max-width: px2rem(500px);
|
||||
|
||||
$zindex-modal: 100;
|
||||
$zindex-gototop: 90;
|
||||
@@ -177,6 +178,7 @@ $grid-columns: 12;
|
||||
|
||||
$header-height: 5rem;
|
||||
$aside-width: 20rem;
|
||||
$hr-margin-y: 2rem;
|
||||
|
||||
$form-focus-color: $color-primary;
|
||||
$form-check-size: 1rem;
|
||||
|
||||
@@ -72,6 +72,11 @@ $form-range-thumb-transition: background-color .3s ease-in-out, bor
|
||||
}
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-weight: $font-weight-medium;
|
||||
margin-bottom: 0.5rem
|
||||
}
|
||||
|
||||
.form-check {
|
||||
@extend %form-common;
|
||||
@@ -135,7 +140,9 @@ $form-range-thumb-transition: background-color .3s ease-in-out, bor
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.form-footer {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
|
||||
.form-range {
|
||||
@@ -346,3 +353,57 @@ $form-range-thumb-transition: background-color .3s ease-in-out, bor
|
||||
border-color: $color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
/* Switch */
|
||||
.form-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
height: 1.5rem;
|
||||
width: 2.75rem;
|
||||
}
|
||||
|
||||
.form-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: $color-primary;
|
||||
}
|
||||
|
||||
input:focus + .slider {
|
||||
box-shadow: 0 0 1px $color-primary;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
-webkit-transform: translateX(1rem);
|
||||
-ms-transform: translateX(1rem);
|
||||
transform: translateX(1rem);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
.row > * {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.container,
|
||||
.container-fluid {
|
||||
width: 100%;
|
||||
@@ -10,6 +14,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.container-tight {
|
||||
max-width: $container-tight-max-width;
|
||||
}
|
||||
|
||||
.container-narrow {
|
||||
max-width: $container-narrow-max-width;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,40 @@ hr {
|
||||
border-top: 1px solid $color-border-light;
|
||||
}
|
||||
|
||||
.hr-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: $hr-margin-y 0;
|
||||
height: 1px;
|
||||
|
||||
&:after,
|
||||
&:before {
|
||||
flex: 1 1 auto;
|
||||
height: 1px;
|
||||
background-color: $color-border;
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
margin-left: .5rem;
|
||||
}
|
||||
|
||||
> *:first-child {
|
||||
padding-right: .5rem;
|
||||
padding-left: 0;
|
||||
color: $color-border;
|
||||
}
|
||||
|
||||
.card > & {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $color-primary;
|
||||
text-decoration: none;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { type Session } from 'next-auth';
|
||||
import { Prisma } from '@prisma/client'
|
||||
|
||||
export type IconType = {
|
||||
name: string
|
||||
tags: string[]
|
||||
@@ -18,3 +21,37 @@ export type DocsItem = {
|
||||
}
|
||||
|
||||
export type DocsConfigType = DocsItem[]
|
||||
|
||||
export type ExtendedSession = Session & { user?: { id?: string} }
|
||||
|
||||
const userWithRelations = Prisma.validator<Prisma.UserDefaultArgs>()({
|
||||
include: { accounts: true, sessions: true, subscription: true },
|
||||
})
|
||||
|
||||
export type User = Prisma.UserGetPayload<typeof userWithRelations>
|
||||
|
||||
const planWithRelations = Prisma.validator<Prisma.PlanDefaultArgs>()({
|
||||
include: { subscriptions: true },
|
||||
})
|
||||
|
||||
export type Plan = Prisma.PlanGetPayload<typeof planWithRelations>
|
||||
|
||||
const subscriptionWithRelations = Prisma.validator<Prisma.SubscriptionDefaultArgs>()({
|
||||
include: { plan: true, user: true },
|
||||
})
|
||||
|
||||
export type Subscription = Prisma.SubscriptionGetPayload<typeof subscriptionWithRelations>
|
||||
|
||||
export type SubscriptionState = {
|
||||
id?: number,
|
||||
planName?: string,
|
||||
planInterval?: string,
|
||||
productId?: number,
|
||||
variantId?: number,
|
||||
status?: string,
|
||||
renewalDate?: Date | null,
|
||||
trialEndDate?: Date | null,
|
||||
expiryDate?: Date | null,
|
||||
unpauseDate?: Date | null,
|
||||
price?: number,
|
||||
} | undefined
|
||||
Reference in New Issue
Block a user