Update to Next.js 14 and latest dependencies. (#186)

This commit is contained in:
Lee Robinson 2023-11-26 20:15:36 -06:00 committed by GitHub
commit 35e83dc87e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 1969 additions and 2012 deletions

View file

@ -8,7 +8,8 @@
],
"plugins": ["tailwindcss"],
"rules": {
"tailwindcss/no-custom-classname": "off"
"tailwindcss/no-custom-classname": "off",
"tailwindcss/classnames-order": "off"
},
"settings": {
"tailwindcss": {

2
.gitignore vendored
View file

@ -32,7 +32,7 @@ yarn-error.log*
# turbo
.turbo
.contentlayer
.env
.vercel
.vscode
.env*.local

View file

@ -1,5 +1,5 @@
<a href="https://chat.vercel.ai/">
<img alt="Next.js 13 and app template Router-ready AI chatbot." src="https://chat.vercel.ai/opengraph-image.png">
<img alt="Next.js 14 and App Router-ready AI chatbot." src="https://chat.vercel.ai/opengraph-image.png">
<h1 align="center">Next.js AI Chatbot</h1>
</a>
@ -22,7 +22,6 @@
- React Server Components (RSCs), Suspense, and Server Actions
- [Vercel AI SDK](https://sdk.vercel.ai/docs) for streaming chat UI
- Support for OpenAI (default), Anthropic, Cohere, Hugging Face, or custom AI chat models and/or LangChain
- Edge runtime-ready
- [shadcn/ui](https://ui.shadcn.com)
- Styling with [Tailwind CSS](https://tailwindcss.com)
- [Radix UI](https://radix-ui.com) for headless component primitives
@ -46,7 +45,6 @@ Follow the steps outlined in the [quick start guide](https://vercel.com/docs/sto
Remember to update your environment variables (`KV_URL`, `KV_REST_API_URL`, `KV_REST_API_TOKEN`, `KV_REST_API_READ_ONLY_TOKEN`) in the `.env` file with the appropriate credentials provided during the KV database setup.
## Running locally
You will need to use the environment variables [defined in `.env.example`](.env.example) to run Next.js AI Chatbot. It's recommended you use [Vercel Environment Variables](https://vercel.com/docs/projects/environment-variables) for this, but a `.env` file is all that is necessary.

View file

@ -1,18 +1,16 @@
import { kv } from '@vercel/kv'
import { OpenAIStream, StreamingTextResponse } from 'ai'
import { Configuration, OpenAIApi } from 'openai-edge'
import OpenAI from 'openai'
import { auth } from '@/auth'
import { nanoid } from '@/lib/utils'
export const runtime = 'edge'
const configuration = new Configuration({
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY
})
const openai = new OpenAIApi(configuration)
export async function POST(req: Request) {
const json = await req.json()
const { messages, previewToken } = json
@ -25,10 +23,10 @@ export async function POST(req: Request) {
}
if (previewToken) {
configuration.apiKey = previewToken
openai.apiKey = previewToken
}
const res = await openai.createChatCompletion({
const res = await openai.chat.completions.create({
model: 'gpt-3.5-turbo',
messages,
temperature: 0.7,

View file

@ -5,9 +5,6 @@ import { auth } from '@/auth'
import { getChat } from '@/app/actions'
import { Chat } from '@/components/chat'
export const runtime = 'edge'
export const preferredRegion = 'home'
export interface ChatPageProps {
params: {
id: string

View file

@ -1,24 +1,20 @@
import { Metadata } from 'next'
import { Toaster } from 'react-hot-toast'
import { GeistSans } from 'geist/font/sans'
import { GeistMono } from 'geist/font/mono'
import '@/app/globals.css'
import { fontMono, fontSans } from '@/lib/fonts'
import { cn } from '@/lib/utils'
import { TailwindIndicator } from '@/components/tailwind-indicator'
import { Providers } from '@/components/providers'
import { Header } from '@/components/header'
export const metadata: Metadata = {
export const metadata = {
metadataBase: new URL(`https://${process.env.VERCEL_URL}`),
title: {
default: 'Next.js AI Chatbot',
template: `%s - Next.js AI Chatbot`
},
description: 'An AI-powered chatbot template built with Next.js and Vercel.',
themeColor: [
{ media: '(prefers-color-scheme: light)', color: 'white' },
{ media: '(prefers-color-scheme: dark)', color: 'black' }
],
icons: {
icon: '/favicon.ico',
shortcut: '/favicon-16x16.png',
@ -26,6 +22,13 @@ export const metadata: Metadata = {
}
}
export const viewport = {
themeColor: [
{ media: '(prefers-color-scheme: light)', color: 'white' },
{ media: '(prefers-color-scheme: dark)', color: 'black' }
]
}
interface RootLayoutProps {
children: React.ReactNode
}
@ -33,18 +36,16 @@ interface RootLayoutProps {
export default function RootLayout({ children }: RootLayoutProps) {
return (
<html lang="en" suppressHydrationWarning>
<head />
<body
className={cn(
'font-sans antialiased',
fontSans.variable,
fontMono.variable
GeistSans.variable,
GeistMono.variable
)}
>
<Toaster />
<Providers attribute="class" defaultTheme="system" enableSystem>
<div className="flex flex-col min-h-screen">
{/* @ts-ignore */}
<Header />
<main className="flex flex-col flex-1 bg-muted/50">{children}</main>
</div>

View file

@ -1,8 +1,6 @@
import { nanoid } from '@/lib/utils'
import { Chat } from '@/components/chat'
export const runtime = 'edge'
export default function IndexPage() {
const id = nanoid()

View file

@ -1,118 +0,0 @@
import { ImageResponse } from 'next/server'
import { getSharedChat } from '@/app/actions'
export const runtime = 'edge'
export const alt = 'AI Chatbot'
export const size = {
width: 1200,
height: 630
}
export const contentType = 'image/png'
const interRegular = fetch(
new URL('../../../assets/fonts/Inter-Regular.woff', import.meta.url)
).then(res => res.arrayBuffer())
const interBold = fetch(
new URL('../../../assets/fonts/Inter-Bold.woff', import.meta.url)
).then(res => res.arrayBuffer())
interface ImageProps {
params: {
id: string
}
}
export default async function Image({ params }: ImageProps) {
const chat = await getSharedChat(params.id)
if (!chat || !chat?.sharePath) {
return null
}
const textAlign = chat?.title?.length > 40 ? 'items-start' : 'items-center'
return new ImageResponse(
(
<div tw="flex w-full items-start h-full flex-col bg-[#09090b] text-white p-[80px]">
<div tw="flex flex-col w-full pt-[40px]">
<div tw={`flex w-full ${textAlign}`}>
<div tw="flex h-18 w-18 items-center justify-center rounded-md border border-[#9b9ba4]">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
fill="currentColor"
width={48}
height={48}
>
<path d="M230.92 212c-15.23-26.33-38.7-45.21-66.09-54.16a72 72 0 1 0-73.66 0c-27.39 8.94-50.86 27.82-66.09 54.16a8 8 0 1 0 13.85 8c18.84-32.56 52.14-52 89.07-52s70.23 19.44 89.07 52a8 8 0 1 0 13.85-8ZM72 96a56 56 0 1 1 56 56 56.06 56.06 0 0 1-56-56Z"></path>
</svg>
</div>
<div tw="flex text-white font-bold text-4xl leading-normal ml-10">
{chat.title.length > 120
? `${chat.title.slice(0, 120)}...`
: chat.title}
</div>
</div>
<div tw="flex w-full mt-14 items-start">
<div tw="flex h-18 w-18 items-center justify-center rounded-md border border-[#9b9ba4]">
<svg
fill="currentColor"
viewBox="0 0 24 24"
role="img"
xmlns="http://www.w3.org/2000/svg"
width={48}
height={48}
>
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"></path>
</svg>
</div>
<div tw="flex text-white font-bold text-6xl leading-none ml-10">
...
</div>
</div>
</div>
<div tw="flex items-center justify-between w-full mt-auto">
<div tw="flex items-center">
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
fill="#9b9ba4"
width={40}
height={40}
>
<path d="M24 22.525H0l12-21.05 12 21.05z" />
</svg>
<div tw="flex text-[1.8rem] ml-4 text-[#9b9ba4]">
Built with{' '}
<div tw="flex text-[#eaeaf0] ml-2 mr-2">Vercel AI SDK</div> &
<div tw="flex text-[#eaeaf0] ml-2">KV</div>
</div>
</div>
<div tw="text-[1.8rem] ml-auto text-[#9b9ba4]">chat.vercel.ai</div>
</div>
</div>
),
{
...size,
fonts: [
{
name: 'Inter',
data: await interRegular,
style: 'normal',
weight: 400
},
{
name: 'Inter',
data: await interBold,
style: 'normal',
weight: 700
}
]
}
)
}

View file

@ -6,9 +6,6 @@ import { getSharedChat } from '@/app/actions'
import { ChatList } from '@/components/chat-list'
import { FooterText } from '@/components/footer'
export const runtime = 'edge'
export const preferredRegion = 'home'
interface SharePageProps {
params: {
id: string

Binary file not shown.

Binary file not shown.

View file

@ -12,8 +12,7 @@ declare module 'next-auth' {
export const {
handlers: { GET, POST },
auth,
CSRF_experimental // will be removed in future
auth
} = NextAuth({
providers: [GitHub],
callbacks: {
@ -24,6 +23,12 @@ export const {
}
return token
},
session: ({ session, token }) => {
if (session?.user && token?.id) {
session.user.id = String(token.id)
}
return session
},
authorized({ auth }) {
return !!auth?.user // this ensures there is a logged in user for -every- request
}

View file

@ -17,40 +17,48 @@ import { SidebarFooter } from '@/components/sidebar-footer'
import { ThemeToggle } from '@/components/theme-toggle'
import { ClearHistory } from '@/components/clear-history'
import { UserMenu } from '@/components/user-menu'
import { LoginButton } from '@/components/login-button'
export async function Header() {
async function UserOrLogin() {
const session = await auth()
return (
<>
{session?.user ? (
<Sidebar>
<React.Suspense fallback={<div className="flex-1 overflow-auto" />}>
<SidebarList userId={session?.user?.id} />
</React.Suspense>
<SidebarFooter>
<ThemeToggle />
<ClearHistory clearChats={clearChats} />
</SidebarFooter>
</Sidebar>
) : (
<Link href="/" target="_blank" rel="nofollow">
<IconNextChat className="w-6 h-6 mr-2 dark:hidden" inverted />
<IconNextChat className="hidden w-6 h-6 mr-2 dark:block" />
</Link>
)}
<div className="flex items-center">
<IconSeparator className="w-6 h-6 text-muted-foreground/50" />
{session?.user ? (
<UserMenu user={session.user} />
) : (
<Button variant="link" asChild className="-ml-2">
<Link href="/sign-in?callbackUrl=/">Login</Link>
</Button>
)}
</div>
</>
)
}
export function Header() {
return (
<header className="sticky top-0 z-50 flex items-center justify-between w-full h-16 px-4 border-b shrink-0 bg-gradient-to-b from-background/10 via-background/50 to-background/80 backdrop-blur-xl">
<div className="flex items-center">
{session?.user ? (
<Sidebar>
<React.Suspense fallback={<div className="flex-1 overflow-auto" />}>
{/* @ts-ignore */}
<SidebarList userId={session?.user?.id} />
</React.Suspense>
<SidebarFooter>
<ThemeToggle />
<ClearHistory clearChats={clearChats} />
</SidebarFooter>
</Sidebar>
) : (
<Link href="/" target="_blank" rel="nofollow">
<IconNextChat className="w-6 h-6 mr-2 dark:hidden" inverted />
<IconNextChat className="hidden w-6 h-6 mr-2 dark:block" />
</Link>
)}
<div className="flex items-center">
<IconSeparator className="w-6 h-6 text-muted-foreground/50" />
{session?.user ? (
<UserMenu user={session.user} />
) : (
<Button variant="link" asChild className="-ml-2">
<Link href="/sign-in?callbackUrl=/">Login</Link>
</Button>
)}
</div>
<React.Suspense fallback={<div className="flex-1 overflow-auto" />}>
<UserOrLogin />
</React.Suspense>
</div>
<div className="flex items-center justify-end space-x-2">
<a

View file

@ -121,7 +121,8 @@ export function SidebarActions({
<div className="space-y-1 rounded-md border p-4 text-sm">
<div className="font-medium">{chat.title}</div>
<div className="text-muted-foreground">
{formatDate(chat.createdAt)} · {chat.messages.length} messages
{formatDate(Number(chat.createdAt))} · {chat.messages.length}{' '}
messages
</div>
</div>
<DialogFooter className="items-center">

View file

@ -1,3 +0,0 @@
'use client'
export { Toaster } from 'react-hot-toast'

View file

@ -10,26 +10,15 @@ const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = ({
className,
children,
...props
}: AlertDialogPrimitive.AlertDialogPortalProps) => (
<AlertDialogPrimitive.Portal className={cn(className)} {...props}>
<div className="fixed inset-0 z-50 flex items-end justify-center sm:items-center">
{children}
</div>
</AlertDialogPrimitive.Portal>
)
AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, children, ...props }, ref) => (
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-opacity animate-in fade-in',
'fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
@ -47,7 +36,7 @@ const AlertDialogContent = React.forwardRef<
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
'fixed z-50 grid w-full max-w-lg scale-100 gap-4 border bg-background p-6 opacity-100 shadow-lg animate-in fade-in-90 slide-in-from-bottom-10 sm:rounded-lg sm:zoom-in-90 sm:slide-in-from-bottom-0 md:w-full',
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className
)}
{...props}
@ -139,6 +128,8 @@ AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,

View file

@ -10,18 +10,9 @@ const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = ({
className,
children,
...props
}: DialogPrimitive.DialogPortalProps) => (
<DialogPrimitive.Portal className={cn(className)} {...props}>
<div className="fixed inset-0 z-50 flex items-start justify-center sm:items-center">
{children}
</div>
</DialogPrimitive.Portal>
)
DialogPortal.displayName = DialogPrimitive.Portal.displayName
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
@ -30,7 +21,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
'fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in',
'fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
@ -47,14 +38,14 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed z-50 grid w-full gap-4 rounded-b-lg border bg-background p-6 shadow-sm animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0',
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<IconClose />
<IconClose className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
@ -119,6 +110,9 @@ DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,

View file

@ -2,9 +2,10 @@
import * as React from 'react'
import * as SheetPrimitive from '@radix-ui/react-dialog'
import { cva, type VariantProps } from 'class-variance-authority'
import { IconClose } from '@/components/ui/icons'
import { cn } from '@/lib/utils'
import { IconClose } from '@/components/ui/icons'
const Sheet = SheetPrimitive.Root
@ -12,27 +13,15 @@ const SheetTrigger = SheetPrimitive.Trigger
const SheetClose = SheetPrimitive.Close
const SheetPortal = ({
className,
children,
...props
}: SheetPrimitive.DialogPortalProps) => (
<SheetPrimitive.Portal
className={cn('fixed inset-0 z-50 flex', className)}
{...props}
>
{children}
</SheetPrimitive.Portal>
)
SheetPortal.displayName = SheetPrimitive.Portal.displayName
const SheetPortal = SheetPrimitive.Portal
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, children, ...props }, ref) => (
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in',
'fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
@ -41,22 +30,43 @@ const SheetOverlay = React.forwardRef<
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
const sheetVariants = cva(
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
{
variants: {
side: {
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
bottom:
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
right:
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm'
}
},
defaultVariants: {
side: 'right'
}
}
)
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>
>(({ className, children, ...props }, ref) => (
SheetContentProps
>(({ side = 'left', className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(
'fixed inset-y-0 left-0 z-50 h-full border-r bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left data-[state=closed]:duration-300 data-[state=open]:duration-500 sm:max-w-sm',
className
)}
className={cn(sheetVariants({ side }), className)}
{...props}
>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<IconClose />
<IconClose className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
@ -68,7 +78,13 @@ const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn('flex flex-col space-y-2', className)} {...props} />
<div
className={cn(
'flex flex-col space-y-2 text-center sm:text-left',
className
)}
{...props}
/>
)
SheetHeader.displayName = 'SheetHeader'
@ -112,6 +128,8 @@ SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,

View file

@ -34,7 +34,8 @@ export function UserMenu({ user }: UserMenuProps) {
className="w-6 h-6 transition-opacity duration-300 rounded-full select-none ring-1 ring-zinc-100/10 hover:opacity-80"
src={user?.image ? `${user.image}&s=60` : ''}
alt={user.name ?? 'Avatar'}
height={48} width={48}
height={48}
width={48}
/>
) : (
<div className="flex items-center justify-center text-xs font-medium uppercase rounded-full select-none h-7 w-7 shrink-0 bg-muted/50 text-muted-foreground">

View file

@ -1,62 +0,0 @@
import type { NextApiRequest } from 'next'
import type { NextFetchEvent, NextRequest } from 'next/server'
export const initAnalytics = ({
request,
event
}: {
request: NextRequest | NextApiRequest | Request
event?: NextFetchEvent
}) => {
const endpoint = process.env.VERCEL_URL
return {
track: async (eventName: string, data?: any) => {
try {
if (!endpoint && process.env.NODE_ENV === 'development') {
console.log(
`[Vercel Web Analytics] Track "${eventName}"` +
(data ? ` with data ${JSON.stringify(data || {})}` : '')
)
return
}
const headers: { [key: string]: string } = {}
Object.entries(request.headers).map(([key, value]) => {
headers[key] = value
})
const body = {
o: headers.referer,
ts: new Date().getTime(),
r: '',
en: eventName,
ed: data
}
const promise = fetch(
`https://${process.env.VERCEL_URL}/_vercel/insights/event`,
{
headers: {
'content-type': 'application/json',
'user-agent': headers['user-agent'] as string,
'x-forwarded-for': headers['x-forwarded-for'] as string,
'x-va-server': '1'
},
body: JSON.stringify(body),
method: 'POST'
}
)
if (event) {
event.waitUntil(promise)
}
{
await promise
}
} catch (err) {
console.error(err)
}
}
}
}

View file

@ -1,11 +0,0 @@
import { JetBrains_Mono as FontMono, Inter as FontSans } from 'next/font/google'
export const fontSans = FontSans({
subsets: ['latin'],
variable: '--font-sans'
})
export const fontMono = FontMono({
subsets: ['latin'],
variable: '--font-mono'
})

View file

@ -1,9 +1,5 @@
/** @type {import('next').NextConfig} */
module.exports = {
reactStrictMode: true,
experimental: {
serverActions: true,
},
images: {
remotePatterns: [
{

View file

@ -1,9 +1,7 @@
{
"name": "next-template",
"version": "0.0.2",
"private": true,
"scripts": {
"dev": "next dev",
"dev": "next dev --turbo",
"build": "next build",
"start": "next start",
"lint": "next lint",
@ -14,55 +12,56 @@
"format:check": "prettier --check \"{app,lib,components}**/*.{ts,tsx,mdx}\" --cache"
},
"dependencies": {
"@radix-ui/react-alert-dialog": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-select": "^1.2.2",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.6",
"@vercel/analytics": "^1.0.0",
"@vercel/kv": "^0.2.1",
"@vercel/og": "^0.5.7",
"ai": "^2.1.6",
"class-variance-authority": "^0.4.0",
"clsx": "^1.2.1",
"focus-trap-react": "^10.1.1",
"nanoid": "^4.0.2",
"next": "13.4.7-canary.1",
"next-auth": "0.0.0-manual.83c4ebd1",
"@radix-ui/react-tooltip": "^1.0.7",
"@vercel/analytics": "^1.1.1",
"@vercel/kv": "^1.0.0",
"@vercel/og": "^0.5.20",
"ai": "^2.2.25",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"focus-trap-react": "^10.2.3",
"geist": "^1.1.0",
"nanoid": "^5.0.3",
"next": "14.0.4-canary.17",
"next-auth": "5.0.0-beta.3",
"next-themes": "^0.2.1",
"openai-edge": "^0.5.1",
"openai": "^4.20.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1",
"react-intersection-observer": "^9.4.4",
"react-intersection-observer": "^9.5.3",
"react-markdown": "^8.0.7",
"react-syntax-highlighter": "^15.5.0",
"react-textarea-autosize": "^8.4.1",
"react-textarea-autosize": "^8.5.3",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.9",
"@types/node": "^17.0.12",
"@types/react": "^18.0.22",
"@types/react-dom": "^18.0.7",
"@types/react-syntax-highlighter": "^15.5.6",
"@typescript-eslint/parser": "^5.59.7",
"autoprefixer": "^10.4.13",
"eslint": "^8.31.0",
"eslint-config-next": "13.4.7-canary.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-tailwindcss": "^3.12.0",
"postcss": "^8.4.21",
"prettier": "^2.7.1",
"tailwind-merge": "^1.12.0",
"tailwindcss": "^3.3.1",
"tailwindcss-animate": "^1.0.5",
"typescript": "^5.1.3"
"@tailwindcss/typography": "^0.5.10",
"@types/node": "^20.10.0",
"@types/react": "^18.2.38",
"@types/react-dom": "^18.2.17",
"@types/react-syntax-highlighter": "^15.5.10",
"@typescript-eslint/parser": "^6.12.0",
"autoprefixer": "^10.4.16",
"eslint": "^8.54.0",
"eslint-config-next": "14.0.3",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-tailwindcss": "^3.13.0",
"postcss": "^8.4.31",
"prettier": "^3.1.0",
"tailwind-merge": "^2.0.0",
"tailwindcss": "^3.3.5",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.3.2"
},
"packageManager": "pnpm@8.6.3"
}

3457
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,3 @@
const { fontFamily } = require('tailwindcss/defaultTheme')
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ['class'],
@ -14,7 +12,8 @@ module.exports = {
},
extend: {
fontFamily: {
sans: ['var(--font-sans)', ...fontFamily.sans]
sans: ['var(--font-geist-sans)'],
mono: ['var(--font-geist-mono)']
},
colors: {
border: 'hsl(var(--border))',