1
0
mirror of https://github.com/tabler/tabler.git synced 2025-12-21 17:34:25 +04:00

Compare commits

...

26 Commits

Author SHA1 Message Date
tabler.developer@gmail.com
95891cb108 remove old lemon squeezy integration 2023-12-07 02:07:01 +01:00
tabler.developer@gmail.com
09a8e2d2c5 lemon squeezy integration 2023-12-07 00:56:02 +01:00
tabler.developer@gmail.com
e4c76be517 fix lemon squeezy integration 2023-11-12 23:21:31 +01:00
tabler.developer@gmail.com
9bbdba9c67 add lemon squeezy webhooks 2023-11-08 01:39:58 +01:00
tabler.developer@gmail.com
4ef2d125c2 WIP add lemon squeezy webhooks 2023-10-26 22:12:50 +02:00
tabler.developer@gmail.com
7193a70102 pricing cards for lemon squeezy products 2023-10-20 02:00:26 +02:00
tabler.developer@gmail.com
442ac3bb4b fetch lemon squezy products 2023-10-09 00:44:43 +02:00
tabler.developer@gmail.com
877182140d Merge branch 'dev' into dev-site-entry 2023-10-08 14:34:08 +02:00
tabler.developer@gmail.com
9772160071 fix build 2023-09-09 01:46:05 +02:00
tabler.developer@gmail.com
aeff172a41 init subscriptions 2023-09-09 01:29:20 +02:00
tabler.developer@gmail.com
7e62c3a563 add navigation auth 2023-08-31 02:11:31 +02:00
tabler.developer@gmail.com
d673851db5 replace credentails auth with auth0 2023-08-28 00:16:54 +02:00
tabler.developer@gmail.com
020255f161 auth error handling 2023-08-27 00:44:28 +02:00
tabler.developer@gmail.com
5edc93384c fix error message 2023-08-21 22:30:49 +02:00
tabler.developer@gmail.com
0efbb01e55 fix package.json 2023-08-20 22:33:23 +02:00
tabler.developer@gmail.com
361e81e478 postinstall prisma generate 2023-08-20 22:25:44 +02:00
tabler.developer@gmail.com
ebda434060 add prisma generate 2023-08-20 22:03:49 +02:00
tabler.developer@gmail.com
8ffe0e6a1a auth fixes 2023-08-20 20:49:48 +02:00
tabler.developer@gmail.com
5250158600 remove nextauth url 2023-08-20 16:51:43 +02:00
tabler.developer@gmail.com
e307ba44fb add credentials provider 2023-08-20 16:43:59 +02:00
tabler.developer@gmail.com
8cf5058456 add google-brand icon 2023-08-15 21:50:59 +02:00
tabler.developer@gmail.com
b7c772ce1b styles fix 2023-08-15 21:18:11 +02:00
tabler.developer@gmail.com
1f0e6e074a add google provider 2023-08-15 16:26:15 +02:00
tabler.developer@gmail.com
a76df72359 prisma and auth init 2023-08-15 00:30:16 +02:00
tabler.developer@gmail.com
90f4931c96 tabler signup and signin 2023-08-12 18:05:13 +02:00
tabler.developer@gmail.com
33bbc46229 init tabler site entry 2023-08-08 23:09:36 +02:00
45 changed files with 2793 additions and 149 deletions

View File

@@ -1,4 +1,4 @@
{
"presets": ["next/babel"],
"plugins": []
"plugins": ["@babel/plugin-transform-private-methods"]
}

View File

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

@@ -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/*

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

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

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

View 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">&larr; 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>
)
}

View 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>
)
}

View 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>
)
}

View 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');
}

View 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};

View 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})
}
}

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

View 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 })
}

View File

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

View File

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

View File

@@ -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
View 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} />
)
}

View File

@@ -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');
}

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

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

View 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 &rarr;
</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&apos;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 &rarr;
</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&apos;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'
})
}

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

View File

@@ -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 = () => {

View File

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

@@ -0,0 +1,3 @@
export { default } from 'next-auth/middleware';
export const config = { matcher: ['/billing'] };

View File

@@ -10,6 +10,8 @@ const nextConfig = {
domains: ["avatars.githubusercontent.com"],
},
experimental: {
appDir: true,
serverActions: true,
},
async redirects() {
return JSON.parse(fs.readFileSync('./redirects.json'))

View File

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

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

View 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;

View 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
View 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")
}

View File

@@ -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;

View File

@@ -21,6 +21,10 @@ Cards
.card-body {
padding: $gap-4;
flex: 1;
.card-md > & {
padding: 2.5rem;
}
}
.card-title {

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;

View File

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