Built with the Next.js 13.5 App Router, tRPC, TypeScript, Prisma & Tailwind
polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAElBMVEUAAAD8/vz08vT09vT8+vzs7uxH16TeAAAAAXRSTlMAQObYZgAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAuFJREFUOI0Vk+3NLiEIRG1B8ClAYAsQ2AIEt4D9ePtv5Xp/mZgYJ2fOFJKEfInkVWY2aglmQFkimRTV7MblYyVqD7HXyhKsSuPX12MeDhRHLtGvRG+P+B/S0Vu4OswR9tmvwNPyhdCDbVayJGads/WiUWcjCvCnruTBNHS9gmX2VzVbk7ZvB1gb1hkWFGl+A/n+/FowcO34U/XvKqZ/fHY+6vgRfU92XrOBUbGeeDfQmjWjdrK+frc6FdGReQhfSF5JvR29O2QrfNw1huTwlgsyXLo0u+5So82sgv7tsFZR2nxB6lXiquHrfD8nfYZ9SeT0LiuvSoVrxGY16pCNRZKqvwWsn5OHypPBELzohMCaRaa0ceTHYqe7X/gfJEEtKFbJpWoNqO+aS1cuTykGPpK5Ga48m6L3NefTr013KqYBQu929iP1oQ/7UwSR+i3zqruUmT84qmhzLpxyj7pr9kg7LKvqaXxZmdpn+6o8sHqSqojy02gU3U8q9PnpidiaLks0mbMYz+q2uVXsoBQ8bfURULYxRgZVYCHMv9F4OA7qxT2NPPpvGQ/sTDH2yznKh7E2AcErfcNsaIoN1izzbJiaY63x4QjUFdBSvDCvugPpu5xDny0jzEeuUQbcP1aGT9V90uixngTRLYNEIIZ6yOF1H8tm7rj2JxiefsVy53zGVy3ag5uuPsdufYOzYxLRxngKe7nhx3VAq54pmz/DK9/Q3aDam2Yt3hNXB4HuU87jKNd/CKZn77Qdn5QkXPfqSkhk7hGOXXB+7v09KbBbqdvxGqa0AqfK/atIrL2WXdAgXAJ43Wtwe/aIoacXezeGPMlhDOHDbSfHnaXsL2QzbT82GRwZuezdwcoWzx5pnOnGMUdHuiY7lhdyWzWiHnucLZQxYStMJbtcydHaQ6vtMbe0AcDbxG+QG14AL94xry4297xpy9Cpf1OoxZ740gHDfrK+gtsy0xabwJmfgtCeii79B6aj0SJeLbd7AAAAAElFTkSuQmCC);
import { PrismaClient } from '@prisma/client'
declare global {
// eslint-disable-next-line no-var
var cachedPrisma: PrismaClient
}
let prisma: PrismaClient
if (process.env.NODE_ENV === 'production') {
prisma = new PrismaClient()
} else {
if (!global.cachedPrisma) {
global.cachedPrisma = new PrismaClient()
}
prisma = global.cachedPrisma
}
export const db = prisma
import 'react-pdf/dist/esm/Page/AnnotationLayer.css'
import 'react-pdf/dist/esm/Page/TextLayer.css'
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`
.scrollbar-w-2::-webkit-scrollbar {
width: 0.25rem;
height: 0.25rem;
}
.scrollbar-track-blue-lighter::-webkit-scrollbar-track {
--bg-opacity: 0.5;
background-color: #00000015;
}
.scrollbar-thumb-blue::-webkit-scrollbar-thumb {
--bg-opacity: 0.5;
background-color: #13131374;
}
.scrollbar-thumb-rounded::-webkit-scrollbar-thumb {
border-radius: 7px;
}
import { PineconeClient } from '@pinecone-database/pinecone'
export const getPineconeClient = async () => {
const client = new PineconeClient()
await client.init({
apiKey: process.env.PINECONE_API_KEY!,
environment: 'us-east1-gcp',
})
return client
}
messages: [
{
role: 'system',
content:
'Use the following pieces of context (or previous conversaton if needed) to answer the users question in markdown format.',
},
{
role: 'user',
content: `Use the following pieces of context (or previous conversaton if needed) to answer the users question in markdown format. \nIf you don't know the answer, just say that you don't know, don't try to make up an answer.
\n----------------\n
PREVIOUS CONVERSATION:
${formattedPrevMessages.map((message) => {
if (message.role === 'user') return `User: ${message.content}\n`
return `Assistant: ${message.content}\n`
})}
\n----------------\n
CONTEXT:
${results.map((r) => r.pageContent).join('\n\n')}
USER INPUT: ${message}`,
},
],
<svg {...props} viewBox='0 0 24 24'>
<path d='m6.94 14.036c-.233.624-.43 1.2-.606 1.783.96-.697 2.101-1.139 3.418-1.304 2.513-.314 4.746-1.973 5.876-4.058l-1.456-1.455 1.413-1.415 1-1.001c.43-.43.915-1.224 1.428-2.368-5.593.867-9.018 4.292-11.074 9.818zm10.06-5.035 1 .999c-1 3-4 6-8 6.5-2.669.334-4.336 2.167-5.002 5.5h-1.998c1-6 3-20 18-20-1 2.997-1.998 4.996-2.997 5.997z' />
</svg>
const pricingItems = [
{
plan: 'Free',
tagline: 'For small side projects.',
quota: 10,
features: [
{
text: '5 pages per PDF',
footnote: 'The maximum amount of pages per PDF-file.',
},
{
text: '4MB file size limit',
footnote: 'The maximum file size of a single PDF file.',
},
{
text: 'Mobile-friendly interface',
},
{
text: 'Higher-quality responses',
footnote: 'Better algorithmic responses for enhanced content quality',
negative: true,
},
{
text: 'Priority support',
negative: true,
},
],
},
{
plan: 'Pro',
tagline: 'For larger projects with higher needs.',
quota: PLANS.find((p) => p.slug === 'pro')!.quota,
features: [
{
text: '25 pages per PDF',
footnote: 'The maximum amount of pages per PDF-file.',
},
{
text: '16MB file size limit',
footnote: 'The maximum file size of a single PDF file.',
},
{
text: 'Mobile-friendly interface',
},
{
text: 'Higher-quality responses',
footnote: 'Better algorithmic responses for enhanced content quality',
},
{
text: 'Priority support',
},
],
},
]
import { PLANS } from '@/config/stripe'
import { db } from '@/db'
import { getKindeServerSession } from '@kinde-oss/kinde-auth-nextjs/server'
import Stripe from 'stripe'
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY ?? '', {
apiVersion: '2023-08-16',
typescript: true,
})
export async function getUserSubscriptionPlan() {
const { getUser } = getKindeServerSession()
const user = getUser()
if (!user.id) {
return {
...PLANS[0],
isSubscribed: false,
isCanceled: false,
stripeCurrentPeriodEnd: null,
}
}
const dbUser = await db.user.findFirst({
where: {
id: user.id,
},
})
if (!dbUser) {
return {
...PLANS[0],
isSubscribed: false,
isCanceled: false,
stripeCurrentPeriodEnd: null,
}
}
const isSubscribed = Boolean(
dbUser.stripePriceId &&
dbUser.stripeCurrentPeriodEnd && // 86400000 = 1 day
dbUser.stripeCurrentPeriodEnd.getTime() + 86_400_000 > Date.now()
)
const plan = isSubscribed
? PLANS.find((plan) => plan.price.priceIds.test === dbUser.stripePriceId)
: null
let isCanceled = false
if (isSubscribed && dbUser.stripeSubscriptionId) {
const stripePlan = await stripe.subscriptions.retrieve(
dbUser.stripeSubscriptionId
)
isCanceled = stripePlan.cancel_at_period_end
}
return {
...plan,
stripeSubscriptionId: dbUser.stripeSubscriptionId,
stripeCurrentPeriodEnd: dbUser.stripeCurrentPeriodEnd,
stripeCustomerId: dbUser.stripeCustomerId,
isSubscribed,
isCanceled,
}
}
import { db } from '@/db'
import { stripe } from '@/lib/stripe'
import { headers } from 'next/headers'
import type Stripe from 'stripe'
export async function POST(request: Request) {
const body = await request.text()
const signature = headers().get('Stripe-Signature') ?? ''
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET || ''
)
} catch (err) {
return new Response(
`Webhook Error: ${
err instanceof Error ? err.message : 'Unknown Error'
}`,
{ status: 400 }
)
}
const session = event.data
.object as Stripe.Checkout.Session
if (!session?.metadata?.userId) {
return new Response(null, {
status: 200,
})
}
if (event.type === 'checkout.session.completed') {
const subscription =
await stripe.subscriptions.retrieve(
session.subscription as string
)
}
if (event.type === 'invoice.payment_succeeded') {
// Retrieve the subscription details from Stripe.
const subscription =
await stripe.subscriptions.retrieve(
session.subscription as string
)
}
return new Response(null, { status: 200 })
}