Add sharing (#17)
This commit is contained in:
commit
a9e4956909
32 changed files with 800 additions and 207 deletions
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { kv } from '@vercel/kv'
|
||||
import { currentUser } from '@clerk/nextjs'
|
||||
|
||||
import { type Chat } from '@/lib/types'
|
||||
import { currentUser } from '@clerk/nextjs'
|
||||
|
||||
export async function getChats(userId?: string | null) {
|
||||
if (!userId) {
|
||||
|
|
@ -51,12 +51,64 @@ export async function removeChat({ id, path }: { id: string; path: string }) {
|
|||
}
|
||||
|
||||
const uid = await kv.hget<string>(`chat:${id}`, 'userId')
|
||||
|
||||
if (uid !== user.id) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
|
||||
await kv.del(`chat:${id}`)
|
||||
await kv.zrem(`user:chat:${user.id}`, `chat:${id}`)
|
||||
|
||||
revalidatePath('/')
|
||||
revalidatePath(path)
|
||||
}
|
||||
|
||||
export async function clearChats() {
|
||||
const user = await currentUser()
|
||||
|
||||
if (!user) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
|
||||
const chats: string[] = await kv.zrange(`user:chat:${user.id}`, 0, -1, {
|
||||
rev: true
|
||||
})
|
||||
|
||||
const pipeline = kv.pipeline()
|
||||
|
||||
for (const chat of chats) {
|
||||
pipeline.del(chat)
|
||||
pipeline.zrem(`user:chat:${user.id}`, chat)
|
||||
}
|
||||
|
||||
await pipeline.exec()
|
||||
|
||||
revalidatePath('/')
|
||||
}
|
||||
|
||||
export async function getSharedChat(id: string) {
|
||||
const chat = await kv.hgetall<Chat>(`chat:${id}`)
|
||||
|
||||
if (!chat || !chat.sharePath) {
|
||||
return null
|
||||
}
|
||||
|
||||
return chat
|
||||
}
|
||||
|
||||
export async function shareChat(chat: Chat) {
|
||||
const user = await currentUser()
|
||||
|
||||
if (!user || chat.userId !== user.id) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
|
||||
const payload = {
|
||||
...chat,
|
||||
sharePath: `/share/${chat.id}`
|
||||
}
|
||||
|
||||
await kv.hmset(`chat:${chat.id}`, payload)
|
||||
|
||||
return payload
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,11 +43,13 @@ export async function POST(req: Request) {
|
|||
const userId = user.id
|
||||
const id = json.id ?? nanoid()
|
||||
const createdAt = Date.now()
|
||||
const path = `/chat/${id}`
|
||||
const payload = {
|
||||
id,
|
||||
title,
|
||||
userId,
|
||||
createdAt,
|
||||
path,
|
||||
messages: [
|
||||
...messages,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { Metadata } from 'next'
|
||||
|
||||
import { ClerkProvider } from '@clerk/nextjs'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
|
||||
import '@/app/globals.css'
|
||||
import { fontMono, fontSans } from '@/lib/fonts'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { TailwindIndicator } from '@/components/tailwind-indicator'
|
||||
import { ThemeProvider } from '@/components/theme-provider'
|
||||
import { Providers } from '@/components/providers'
|
||||
import { Header } from '@/components/header'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
|
|
@ -42,14 +43,17 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
|||
fontMono.variable
|
||||
)}
|
||||
>
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||
<Toaster />
|
||||
<Providers attribute="class" defaultTheme="system" enableSystem>
|
||||
<div className="flex min-h-screen flex-col">
|
||||
{/* @ts-ignore */}
|
||||
<Header />
|
||||
<main className="flex-1 bg-muted/50">{children}</main>
|
||||
<main className="flex flex-1 flex-col bg-muted/50">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
<TailwindIndicator />
|
||||
</ThemeProvider>
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
</ClerkProvider>
|
||||
|
|
|
|||
53
app/share/[id]/page.tsx
Normal file
53
app/share/[id]/page.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { type Metadata } from 'next'
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
import { formatDate } from '@/lib/utils'
|
||||
import { getSharedChat } from '@/app/actions'
|
||||
import { ChatList } from '@/components/chat-list'
|
||||
import { FooterText } from '@/components/footer'
|
||||
|
||||
// export const runtime = 'edge'
|
||||
export const preferredRegion = 'home'
|
||||
|
||||
export interface SharePageProps {
|
||||
params: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params
|
||||
}: SharePageProps): Promise<Metadata> {
|
||||
const chat = await getSharedChat(params.id)
|
||||
|
||||
return {
|
||||
title: chat?.title.slice(0, 50) ?? 'Chat'
|
||||
}
|
||||
}
|
||||
|
||||
export default async function SharePage({ params }: SharePageProps) {
|
||||
const chat = await getSharedChat(params.id)
|
||||
|
||||
if (!chat || !chat?.sharePath) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex-1 space-y-6">
|
||||
<div className="border-b bg-background px-4 py-6 md:px-6 md:py-8">
|
||||
<div className="mx-auto max-w-2xl md:px-6">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-2xl font-bold">{chat.title}</h1>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{formatDate(chat.createdAt)} · {chat.messages.length} messages
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ChatList messages={chat.messages} />
|
||||
</div>
|
||||
<FooterText className="py-8" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import { cn } from '@/lib/utils'
|
|||
import { ExternalLink } from '@/components/external-link'
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
import { IconSpinner } from '@/components/ui/icons'
|
||||
import { FooterText } from '@/components/footer'
|
||||
|
||||
export default function SignInPage() {
|
||||
return (
|
||||
|
|
@ -27,14 +28,7 @@ export default function SignInPage() {
|
|||
}
|
||||
}}
|
||||
/>
|
||||
<p className="hidden px-2 text-center text-xs leading-normal text-muted-foreground sm:block">
|
||||
Open source AI chatbot app built with{' '}
|
||||
<ExternalLink href="https://nextjs.org">Next.js</ExternalLink> and{' '}
|
||||
<ExternalLink href="https://vercel.com/storage/kv">
|
||||
Vercel KV
|
||||
</ExternalLink>
|
||||
.
|
||||
</p>
|
||||
<FooterText />
|
||||
</ClerkLoaded>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { cn } from '@/lib/utils'
|
|||
import { buttonVariants } from '@/components/ui/button'
|
||||
import { ExternalLink } from '@/components/external-link'
|
||||
import { IconSpinner } from '@/components/ui/icons'
|
||||
import { FooterText } from '@/components/footer'
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
|
|
@ -28,14 +29,7 @@ export default function Page() {
|
|||
}
|
||||
}}
|
||||
/>
|
||||
<p className="hidden px-2 text-center text-xs leading-normal text-muted-foreground sm:block">
|
||||
Open source AI chatbot app built with{' '}
|
||||
<ExternalLink href="https://nextjs.org">Next.js</ExternalLink> and{' '}
|
||||
<ExternalLink href="https://vercel.com/storage/kv">
|
||||
Vercel KV
|
||||
</ExternalLink>
|
||||
.
|
||||
</p>
|
||||
<FooterText />
|
||||
</ClerkLoaded>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ export function ChatList({ messages }: ChatList) {
|
|||
{messages.map((message, index) => (
|
||||
<div key={index}>
|
||||
<ChatMessage message={message} />
|
||||
{index < messages.length - 1 && <Separator className="my-8" />}
|
||||
{index < messages.length - 1 && (
|
||||
<Separator className="my-4 md:my-8" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { ExternalLink } from '@/components/external-link'
|
|||
import { PromptForm } from '@/components/prompt-form'
|
||||
import { ButtonScrollToBottom } from '@/components/button-scroll-to-bottom'
|
||||
import { IconRefresh, IconStop } from '@/components/ui/icons'
|
||||
import { FooterText } from '@/components/footer'
|
||||
|
||||
export interface ChatPanelProps
|
||||
extends Pick<
|
||||
|
|
@ -70,14 +71,7 @@ export function ChatPanel({
|
|||
setInput={setInput}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<p className="hidden px-2 text-center text-xs leading-normal text-muted-foreground sm:block">
|
||||
Open source AI chatbot app built with{' '}
|
||||
<ExternalLink href="https://nextjs.org">Next.js</ExternalLink> and{' '}
|
||||
<ExternalLink href="https://vercel.com/storage/kv">
|
||||
Vercel KV
|
||||
</ExternalLink>
|
||||
.
|
||||
</p>
|
||||
<FooterText className="hidden sm:block" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,10 +10,11 @@ import { ChatScrollAnchor } from '@/components/chat-scroll-anchor'
|
|||
|
||||
export interface ChatProps extends React.ComponentProps<'div'> {
|
||||
initialMessages?: Message[]
|
||||
|
||||
id?: string
|
||||
}
|
||||
|
||||
export function Chat({ id, initialMessages, className }: ChatProps) {
|
||||
export function Chat({ id, title, initialMessages, className }: ChatProps) {
|
||||
const { messages, append, reload, stop, isLoading, input, setInput } =
|
||||
useChat({
|
||||
initialMessages,
|
||||
|
|
|
|||
65
components/clear-history.tsx
Normal file
65
components/clear-history.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { IconSpinner } from '@/components/ui/icons'
|
||||
|
||||
interface ClearHistoryProps {
|
||||
clearChats: () => Promise<void>
|
||||
}
|
||||
|
||||
export function ClearHistory({ clearChats }: ClearHistoryProps) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [isPending, startTransition] = React.useTransition()
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" disabled={isPending}>
|
||||
{isPending && <IconSpinner className="mr-2" />}
|
||||
Clear history
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will permanently delete your chat history and remove your data
|
||||
from our servers.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isPending}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={isPending}
|
||||
onClick={event => {
|
||||
event.preventDefault()
|
||||
startTransition(async () => {
|
||||
await clearChats()
|
||||
setOpen(false)
|
||||
router.push('/')
|
||||
})
|
||||
}}
|
||||
>
|
||||
{isPending && <IconSpinner className="mr-2 animate-spin" />}
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
23
components/footer.tsx
Normal file
23
components/footer.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import React from 'react'
|
||||
|
||||
import { ExternalLink } from '@/components/external-link'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function FooterText({ className, ...props }: React.ComponentProps<'p'>) {
|
||||
return (
|
||||
<p
|
||||
className={cn(
|
||||
'px-2 text-center text-xs leading-normal text-muted-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
Open source AI chatbot built with{' '}
|
||||
<ExternalLink href="https://nextjs.org">Next.js</ExternalLink> and{' '}
|
||||
<ExternalLink href="https://vercel.com/storage/kv">
|
||||
Vercel KV
|
||||
</ExternalLink>
|
||||
.
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Suspense } from 'react'
|
||||
import { Suspense, use } from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
|
|
@ -6,6 +6,11 @@ import { Sidebar } from '@/components/sidebar'
|
|||
import { SidebarList } from '@/components/sidebar-list'
|
||||
import { IconGitHub, IconSeparator, IconVercel } from '@/components/ui/icons'
|
||||
import { UserButton, currentUser } from '@clerk/nextjs'
|
||||
import { SidebarFooter } from '@/components/sidebar-footer'
|
||||
import { ThemeToggle } from '@/components/theme-toggle'
|
||||
import { ClearHistory } from '@/components/clear-history'
|
||||
import { clearChats } from '@/app/actions'
|
||||
import Link from 'next/link'
|
||||
|
||||
export async function Header() {
|
||||
const user = await currentUser()
|
||||
|
|
@ -14,19 +19,30 @@ export async function Header() {
|
|||
<header className="sticky top-0 z-50 flex h-16 w-full shrink-0 items-center justify-between border-b bg-gradient-to-b from-background/10 via-background/50 to-background/80 px-4 backdrop-blur-xl">
|
||||
<div className="flex items-center">
|
||||
{/* @ts-ignore */}
|
||||
<Sidebar user={user?.id}>
|
||||
{user?.id ? (
|
||||
<Sidebar>
|
||||
<Suspense fallback={<div className="flex-1 overflow-auto" />}>
|
||||
{/* @ts-ignore */}
|
||||
<SidebarList userId={user?.id} />
|
||||
</Suspense>
|
||||
<SidebarFooter>
|
||||
<ThemeToggle />
|
||||
<ClearHistory clearChats={clearChats} />
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
) : (
|
||||
<Link href="https://vercel.com" target="_blank" rel="nofollow">
|
||||
<IconVercel className="mr-2 h-6 w-6" />
|
||||
</Link>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<IconSeparator className="h-6 w-6 text-muted-foreground/50" />
|
||||
{user?.id ? (
|
||||
<UserButton
|
||||
showName
|
||||
appearance={{
|
||||
elements: {
|
||||
afterSignOutUrl='/'
|
||||
afterSignOutUrl: '/',
|
||||
avatarBox: 'w-6 h-6 rounded-full overflow-hidden',
|
||||
userButtonBox: 'flex-row-reverse',
|
||||
userButtonOuterIdentifier: 'text-primary',
|
||||
|
|
@ -43,6 +59,14 @@ export async function Header() {
|
|||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Link
|
||||
href="/sign-in"
|
||||
className={cn(buttonVariants({ variant: 'ghost' }))}
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import { Button, buttonVariants } from '@/components/ui/button'
|
|||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger
|
||||
} from '@/components/ui/tooltip'
|
||||
import { IconArrowElbow, IconPlus } from '@/components/ui/icons'
|
||||
|
|
@ -47,7 +46,6 @@ export function PromptForm({
|
|||
}}
|
||||
ref={formRef}
|
||||
>
|
||||
<TooltipProvider>
|
||||
<div className="relative flex w-full grow flex-col overflow-hidden bg-background px-8 sm:rounded-md sm:border sm:px-12">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
|
@ -91,7 +89,6 @@ export function PromptForm({
|
|||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
14
components/providers.tsx
Normal file
14
components/providers.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
||||
import { ThemeProviderProps } from 'next-themes/dist/types'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
|
||||
export function Providers({ children, ...props }: ThemeProviderProps) {
|
||||
return (
|
||||
<NextThemesProvider {...props}>
|
||||
<TooltipProvider>{children}</TooltipProvider>
|
||||
</NextThemesProvider>
|
||||
)
|
||||
}
|
||||
209
components/sidebar-actions.tsx
Normal file
209
components/sidebar-actions.tsx
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
||||
import { type Chat, ServerActionResult } from '@/lib/types'
|
||||
import { cn, formatDate } from '@/lib/utils'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from '@/components/ui/dialog'
|
||||
import {
|
||||
IconShare,
|
||||
IconSpinner,
|
||||
IconTrash,
|
||||
IconUsers
|
||||
} from '@/components/ui/icons'
|
||||
import Link from 'next/link'
|
||||
import { badgeVariants } from '@/components/ui/badge'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger
|
||||
} from '@/components/ui/tooltip'
|
||||
|
||||
interface SidebarActionsProps {
|
||||
chat: Chat
|
||||
removeChat: (args: { id: string; path: string }) => Promise<void>
|
||||
shareChat: (chat: Chat) => ServerActionResult<Chat>
|
||||
}
|
||||
|
||||
export function SidebarActions({
|
||||
chat,
|
||||
removeChat,
|
||||
shareChat
|
||||
}: SidebarActionsProps) {
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false)
|
||||
const [shareDialogOpen, setShareDialogOpen] = React.useState(false)
|
||||
const [isRemovePending, startRemoveTransition] = React.useTransition()
|
||||
const [isSharePending, startShareTransition] = React.useTransition()
|
||||
const router = useRouter()
|
||||
|
||||
const copyShareLink = React.useCallback(async (chat: Chat) => {
|
||||
if (!chat.sharePath) {
|
||||
return toast.error('Could not copy share link to clipboard')
|
||||
}
|
||||
|
||||
const url = new URL(window.location.href)
|
||||
url.pathname = chat.sharePath
|
||||
navigator.clipboard.writeText(url.toString())
|
||||
setShareDialogOpen(false)
|
||||
toast.success('Share link copied to clipboard', {
|
||||
style: {
|
||||
borderRadius: '10px',
|
||||
background: '#333',
|
||||
color: '#fff',
|
||||
fontSize: '14px'
|
||||
},
|
||||
iconTheme: {
|
||||
primary: 'white',
|
||||
secondary: 'black'
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-x-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-6 w-6 p-0 hover:bg-background"
|
||||
onClick={() => setShareDialogOpen(true)}
|
||||
>
|
||||
<IconShare />
|
||||
<span className="sr-only">Share</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Share chat</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-6 w-6 p-0 hover:bg-background"
|
||||
disabled={isRemovePending}
|
||||
onClick={() => setDeleteDialogOpen(true)}
|
||||
>
|
||||
<IconTrash />
|
||||
<span className="sr-only">Delete</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Delete chat</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Dialog open={shareDialogOpen} onOpenChange={setShareDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Share link to chat</DialogTitle>
|
||||
<DialogDescription>
|
||||
Anyone with the URL will be able to view the shared chat.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<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
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="items-center">
|
||||
{chat.sharePath && (
|
||||
<Link
|
||||
href={chat.sharePath}
|
||||
className={cn(
|
||||
badgeVariants({ variant: 'secondary' }),
|
||||
'mr-auto'
|
||||
)}
|
||||
target="_blank"
|
||||
>
|
||||
<IconUsers className="mr-2" />
|
||||
{chat.sharePath}
|
||||
</Link>
|
||||
)}
|
||||
<Button
|
||||
disabled={isSharePending}
|
||||
onClick={() => {
|
||||
startShareTransition(async () => {
|
||||
if (chat.sharePath) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
copyShareLink(chat)
|
||||
return
|
||||
}
|
||||
|
||||
const result = await shareChat(chat)
|
||||
|
||||
if (!('id' in result)) {
|
||||
toast.error(result.message)
|
||||
return
|
||||
}
|
||||
|
||||
copyShareLink(result)
|
||||
})
|
||||
}}
|
||||
>
|
||||
{isSharePending ? (
|
||||
<>
|
||||
<IconSpinner className="mr-2 animate-spin" />
|
||||
Copying...
|
||||
</>
|
||||
) : (
|
||||
<>Copy link</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will permanently delete your chat message and remove your
|
||||
data from our servers.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isRemovePending}>
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={isRemovePending}
|
||||
onClick={event => {
|
||||
event.preventDefault()
|
||||
startRemoveTransition(async () => {
|
||||
await removeChat({
|
||||
id: chat.id,
|
||||
path: chat.path
|
||||
})
|
||||
setDeleteDialogOpen(false)
|
||||
router.push('/')
|
||||
})
|
||||
}}
|
||||
>
|
||||
{isRemovePending && <IconSpinner className="mr-2 animate-spin" />}
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
16
components/sidebar-footer.tsx
Normal file
16
components/sidebar-footer.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function SidebarFooter({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
className={cn('flex items-center justify-between p-4', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,97 +1,57 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import { usePathname } from 'next/navigation'
|
||||
|
||||
import { type Chat } from '@/lib/types'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
import { IconMessage, IconUsers } from '@/components/ui/icons'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Button, buttonVariants } from '@/components/ui/button'
|
||||
import { IconMessage, IconSpinner, IconTrash } from '@/components/ui/icons'
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger
|
||||
} from '@/components/ui/tooltip'
|
||||
|
||||
interface SidebarItemProps {
|
||||
title: string
|
||||
href: string
|
||||
id: string
|
||||
removeChat: (args: { id: string; path: string }) => Promise<void>
|
||||
chat: Chat
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function SidebarItem({ title, href, id, removeChat }: SidebarItemProps) {
|
||||
const [open, setIsOpen] = React.useState(false)
|
||||
export function SidebarItem({ chat, children }: SidebarItemProps) {
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
const [isPending, startTransition] = React.useTransition()
|
||||
const isActive = pathname === href
|
||||
const isActive = pathname === chat.path
|
||||
|
||||
if (!id) return null
|
||||
if (!chat?.id) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
href={href}
|
||||
href={chat.path}
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'ghost' }),
|
||||
'group w-full px-2',
|
||||
isActive && 'bg-accent'
|
||||
)}
|
||||
>
|
||||
{chat.sharePath ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<IconUsers className="mr-2" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>This is a shared chat.</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<IconMessage className="mr-2" />
|
||||
)}
|
||||
<div
|
||||
className="relative max-h-5 flex-1 select-none overflow-hidden text-ellipsis break-all"
|
||||
title={title}
|
||||
title={chat.title}
|
||||
>
|
||||
<span className="whitespace-nowrap">{title}</span>
|
||||
<span className="whitespace-nowrap">{chat.title}</span>
|
||||
</div>
|
||||
{isActive && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
disabled={isPending}
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<IconTrash />
|
||||
<span className="sr-only">Delete</span>
|
||||
</Button>
|
||||
)}
|
||||
{isActive && children}
|
||||
</Link>
|
||||
<AlertDialog open={open} onOpenChange={setIsOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will permanently delete your chat message and remove your
|
||||
data from our servers.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isPending}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={isPending}
|
||||
onClick={event => {
|
||||
event.preventDefault()
|
||||
startTransition(async () => {
|
||||
await removeChat({ id, path: href })
|
||||
setIsOpen(false)
|
||||
router.push('/')
|
||||
})
|
||||
}}
|
||||
>
|
||||
{isPending && <IconSpinner className="mr-2 animate-spin" />}
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { getChats, removeChat } from '@/app/actions'
|
||||
import { getChats, removeChat, shareChat } from '@/app/actions'
|
||||
import { SidebarActions } from '@/components/sidebar-actions'
|
||||
|
||||
import { SidebarItem } from '@/components/sidebar-item'
|
||||
|
||||
|
|
@ -9,19 +10,24 @@ export interface SidebarListProps {
|
|||
export async function SidebarList({ userId }: SidebarListProps) {
|
||||
const chats = await getChats(userId)
|
||||
|
||||
console.log(chats)
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-auto">
|
||||
{chats?.length ? (
|
||||
<div className="space-y-2 px-2">
|
||||
{chats.map(chat => (
|
||||
<SidebarItem
|
||||
key={chat.id}
|
||||
title={chat.title}
|
||||
href={`/chat/${chat.id}`}
|
||||
id={chat.id}
|
||||
{chats.map(
|
||||
chat =>
|
||||
chat && (
|
||||
<SidebarItem key={chat?.id} chat={chat}>
|
||||
<SidebarActions
|
||||
chat={chat}
|
||||
removeChat={removeChat}
|
||||
shareChat={shareChat}
|
||||
/>
|
||||
))}
|
||||
</SidebarItem>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8 text-center">
|
||||
|
|
|
|||
|
|
@ -10,18 +10,13 @@ import {
|
|||
SheetTitle,
|
||||
SheetTrigger
|
||||
} from '@/components/ui/sheet'
|
||||
import { ThemeToggle } from '@/components/theme-toggle'
|
||||
import { IconSidebar } from '@/components/ui/icons'
|
||||
import { useClerk } from '@clerk/nextjs'
|
||||
|
||||
export interface SidebarProps {
|
||||
userId?: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export function Sidebar({ userId, children }: SidebarProps) {
|
||||
const { signOut } = useClerk()
|
||||
|
||||
export function Sidebar({ children }: SidebarProps) {
|
||||
return (
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
|
|
@ -35,18 +30,6 @@ export function Sidebar({ userId, children }: SidebarProps) {
|
|||
<SheetTitle className="text-sm">Chat History</SheetTitle>
|
||||
</SheetHeader>
|
||||
{children}
|
||||
<div className="flex items-center p-4">
|
||||
<ThemeToggle />
|
||||
{userId && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => signOut()}
|
||||
className="ml-auto"
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
||||
import { ThemeProviderProps } from 'next-themes/dist/types'
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
}
|
||||
3
components/toaster.tsx
Normal file
3
components/toaster.tsx
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
'use client'
|
||||
|
||||
export { Toaster } from 'react-hot-toast'
|
||||
36
components/ui/badge.tsx
Normal file
36
components/ui/badge.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center border rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary hover:bg-primary/80 border-transparent text-primary-foreground",
|
||||
secondary:
|
||||
"bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground",
|
||||
destructive:
|
||||
"bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
|
|
@ -414,6 +414,34 @@ function IconEdit({ className, ...props }: React.ComponentProps<'svg'>) {
|
|||
)
|
||||
}
|
||||
|
||||
function IconShare({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
className={cn('h-4 w-4', className)}
|
||||
viewBox="0 0 256 256"
|
||||
{...props}
|
||||
>
|
||||
<path d="m237.66 106.35-80-80A8 8 0 0 0 144 32v40.35c-25.94 2.22-54.59 14.92-78.16 34.91-28.38 24.08-46.05 55.11-49.76 87.37a12 12 0 0 0 20.68 9.58c11-11.71 50.14-48.74 107.24-52V192a8 8 0 0 0 13.66 5.65l80-80a8 8 0 0 0 0-11.3ZM160 172.69V144a8 8 0 0 0-8-8c-28.08 0-55.43 7.33-81.29 21.8a196.17 196.17 0 0 0-36.57 26.52c5.8-23.84 20.42-46.51 42.05-64.86C99.41 99.77 127.75 88 152 88a8 8 0 0 0 8-8V51.32L220.69 112Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function IconUsers({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
className={cn('h-4 w-4', className)}
|
||||
viewBox="0 0 256 256"
|
||||
{...props}
|
||||
>
|
||||
<path d="M117.25 157.92a60 60 0 1 0-66.5 0 95.83 95.83 0 0 0-47.22 37.71 8 8 0 1 0 13.4 8.74 80 80 0 0 1 134.14 0 8 8 0 0 0 13.4-8.74 95.83 95.83 0 0 0-47.22-37.71ZM40 108a44 44 0 1 1 44 44 44.05 44.05 0 0 1-44-44Zm210.14 98.7a8 8 0 0 1-11.07-2.33A79.83 79.83 0 0 0 172 168a8 8 0 0 1 0-16 44 44 0 1 0-16.34-84.87 8 8 0 1 1-5.94-14.85 60 60 0 0 1 55.53 105.64 95.83 95.83 0 0 1 47.22 37.71 8 8 0 0 1-2.33 11.07Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
IconEdit,
|
||||
IconNextChat,
|
||||
|
|
@ -437,5 +465,7 @@ export {
|
|||
IconCopy,
|
||||
IconCheck,
|
||||
IconDownload,
|
||||
IconClose
|
||||
IconClose,
|
||||
IconShare,
|
||||
IconUsers
|
||||
}
|
||||
|
|
|
|||
26
components/ui/label.tsx
Normal file
26
components/ui/label.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
29
components/ui/switch.tsx
Normal file
29
components/ui/switch.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
export { Switch }
|
||||
|
|
@ -19,7 +19,7 @@ const TooltipContent = React.forwardRef<
|
|||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm font-medium text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1',
|
||||
'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-xs font-medium text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -5,5 +5,9 @@ export interface Chat extends Record<string, any> {
|
|||
title: string
|
||||
createdAt: Date
|
||||
userId: string
|
||||
path: string
|
||||
messages: Message[]
|
||||
sharePath?: string
|
||||
}
|
||||
|
||||
export type ServerActionResult<Result> = Promise<Result | Error>
|
||||
|
|
|
|||
|
|
@ -32,3 +32,12 @@ export async function fetcher<JSON = any>(
|
|||
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export function formatDate(input: string | number | Date): string {
|
||||
const date = new Date(input)
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { authMiddleware } from '@clerk/nextjs'
|
||||
|
||||
// @see https://clerk.dev
|
||||
export default authMiddleware()
|
||||
export default authMiddleware({
|
||||
publicRoutes: ['/share/:id']
|
||||
})
|
||||
|
||||
export const config = {
|
||||
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)']
|
||||
|
|
|
|||
|
|
@ -17,8 +17,10 @@
|
|||
"@clerk/nextjs": "^4.21.1",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@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",
|
||||
|
|
|
|||
68
pnpm-lock.yaml
generated
68
pnpm-lock.yaml
generated
|
|
@ -17,12 +17,18 @@ dependencies:
|
|||
'@radix-ui/react-dialog':
|
||||
specifier: ^1.0.4
|
||||
version: 1.0.4(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-label':
|
||||
specifier: ^2.0.2
|
||||
version: 2.0.2(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-separator':
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-slot':
|
||||
specifier: ^1.0.2
|
||||
version: 1.0.2(@types/react@18.2.6)(react@18.2.0)
|
||||
'@radix-ui/react-switch':
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-tooltip':
|
||||
specifier: ^1.0.6
|
||||
version: 1.0.6(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
|
||||
|
|
@ -709,6 +715,27 @@ packages:
|
|||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-label@2.0.2(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.21.5
|
||||
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@types/react': 18.2.6
|
||||
'@types/react-dom': 18.2.4
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-popper@1.1.2(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==}
|
||||
peerDependencies:
|
||||
|
|
@ -839,6 +866,33 @@ packages:
|
|||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-switch@1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.21.5
|
||||
'@radix-ui/primitive': 1.0.1
|
||||
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.6)(react@18.2.0)
|
||||
'@radix-ui/react-context': 1.0.1(@types/react@18.2.6)(react@18.2.0)
|
||||
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.6)(react@18.2.0)
|
||||
'@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.6)(react@18.2.0)
|
||||
'@radix-ui/react-use-size': 1.0.1(@types/react@18.2.6)(react@18.2.0)
|
||||
'@types/react': 18.2.6
|
||||
'@types/react-dom': 18.2.4
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-tooltip@1.0.6(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-DmNFOiwEc2UDigsYj6clJENma58OelxD24O4IODoZ+3sQc3Zb+L8w1EP+y9laTuKCLAysPw4fD6/v0j4KNV8rg==}
|
||||
peerDependencies:
|
||||
|
|
@ -929,6 +983,20 @@ packages:
|
|||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-use-previous@1.0.1(@types/react@18.2.6)(react@18.2.0):
|
||||
resolution: {integrity: sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.21.5
|
||||
'@types/react': 18.2.6
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-use-rect@1.0.1(@types/react@18.2.6)(react@18.2.0):
|
||||
resolution: {integrity: sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==}
|
||||
peerDependencies:
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ module.exports = {
|
|||
},
|
||||
animation: {
|
||||
'slide-from-left':
|
||||
'slide-from-left 0.4s cubic-bezier(0.82, 0.085, 0.395, 0.895)',
|
||||
'slide-from-left 0.3s cubic-bezier(0.82, 0.085, 0.395, 0.895)',
|
||||
'slide-to-left':
|
||||
'slide-to-left 0.25s cubic-bezier(0.82, 0.085, 0.395, 0.895)',
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue