Chore/server actions (#19200)

* Migrate database-new to use server actions.

* Revert the package-lock.json changes, leftover from a rebased commit.

* Add server actions config back

* Cleanup

* Cleanup

* Undo

---------

Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
Co-authored-by: Terry Sutton <saltcod@gmail.com>
This commit is contained in:
authored and GitHub committed 2023-11-24 13:15:43 -03:30
1 parent 7ce355222b
commit efab162ff9
20 files changed
+382 -315

No files matched your search

@@ -61,6 +61,7 @@ export const Chat = () => {
<div
className={cn(
'bg',
'h-full',
'border-t xl:border-t-0 xl:border-r relative',
'flex flex-col h-full border-r',
'w-full xl:w-[400px] 2xl:w-[500px]'
+53
View File
@@ -0,0 +1,53 @@
'use server'
import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import OpenAI from 'openai'
const openai = new OpenAI()
export async function logout() {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
const { error } = await supabase.auth.signOut()
if (error) {
console.log('Error logging out:', error.message)
return
}
revalidatePath('/', 'layout')
redirect('/')
}
export async function deleteThread(threadID: string) {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
try {
await supabase.from('threads').delete().eq('thread_id', threadID)
} catch (error) {
if (error) console.error('Error deleting thread: ', error)
}
await openai.beta.threads.del(threadID)
revalidatePath('/profile')
}
export async function updateThreadName(threadId: number, threadName: string) {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
try {
await supabase.from('threads').update({ thread_title: threadName }).eq('id', threadId)
} catch (error) {
if (error) console.error('Error updating thread: ', error)
}
revalidatePath('/profile')
}
+6 -8
View File
@@ -19,16 +19,14 @@ export const metadata: Metadata = {
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className="dark">
<body>
<body className="flex flex-col">
<ThemeProvider defaultTheme="system" enableSystem disableTransitionOnChange>
<ReactQueryProvider>
<div className="flex flex-col h-full">
<Header />
<main role="main" className="grow">
{children}
</main>
<Footer />
</div>
<Header />
<main role="main" className="h-full w-full flex flex-col grow">
{children}
</main>
<Footer />
</ReactQueryProvider>
</ThemeProvider>
</body>
@@ -1,35 +1,39 @@
import { Modal } from 'ui'
'use client'
import { Button, Modal } from 'ui'
import { deleteThread } from './../../app/actions'
import { ThreadType } from './Threads'
import { deleteThread } from '@/lib/actions'
const ConfirmDeleteThreadModal = ({
thread,
onClose,
visible,
}: {
thread?: ThreadType
thread: ThreadType
onClose: () => void
visible: boolean
}) => {
const deleteCurrentThread = () => {
const threadID = thread?.thread_id
onClose()
deleteThread(threadID!)
}
return (
<Modal
variant="danger"
alignFooter="right"
size="small"
visible={thread !== undefined}
visible={visible}
onCancel={onClose}
onConfirm={async () => {
await deleteCurrentThread()
}}
hideFooter
header="Confirm to delete thread?"
className="pb-2"
>
<Modal.Content className="py-4">
<p className="text-sm">Once the thread is deleted, it cannot be recovered.</p>
</Modal.Content>
<form action={() => deleteThread(thread.thread_id)}>
<Modal.Content className="py-4">
<p className="text-sm">Once the thread is deleted, it cannot be recovered.</p>
</Modal.Content>
<Modal.Separator />
<Modal.Content className="flex flex-row gap-3 justify-end">
<Button type="default">Cancel</Button>
<Button type="warning" htmlType="submit">
Delete thread
</Button>
</Modal.Content>
</form>
</Modal>
)
}
@@ -1,35 +1,58 @@
import { Input, Modal } from 'ui'
'use client'
import { Button, Input, Modal } from 'ui'
import { updateThreadName } from './../../app/actions'
import { ThreadType } from './Threads'
import { useEffect, useState } from 'react'
import { useState } from 'react'
const EditThreadModal = ({ thread, onClose }: { thread?: ThreadType; onClose: () => void }) => {
const [value, setValue] = useState('')
useEffect(() => {
if (thread !== undefined) setValue(thread.thread_title)
}, [thread])
const updateThread = () => {
// Logic here
onClose()
}
const EditThreadModal = ({
thread,
onClose,
visible,
}: {
thread: ThreadType
onClose: () => void
visible: boolean
}) => {
const [value, setValue] = useState(thread.thread_title)
return (
<Modal
alignFooter="right"
size="medium"
visible={thread !== undefined}
visible={visible}
onCancel={onClose}
onConfirm={updateThread}
hideFooter
header="Edit thread name"
className="pb-2"
>
<Modal.Content className="py-4">
<Input
label="Provide a name for your thread"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</Modal.Content>
<form
action={(formData: FormData) => {
const threadNameEntry: FormDataEntryValue | null = formData.get('threadName')
// Check if threadNameEntry is not null and is of type string
if (threadNameEntry !== null && typeof threadNameEntry === 'string') {
const threadName: string = threadNameEntry
updateThreadName(thread.id, threadName)
}
}}
>
<Modal.Content className="py-4">
<Input
label="Provide a name for your thread"
value={value}
name="threadName"
onChange={(e) => setValue(e.target.value)}
/>
</Modal.Content>
<Modal.Separator />
<Modal.Content className="flex flex-row gap-3 justify-end">
<Button type="default">Cancel</Button>
<Button type="secondary" htmlType="submit">
Update thread name
</Button>
</Modal.Content>
</form>
</Modal>
)
}
+47 -41
View File
@@ -1,4 +1,5 @@
'use client'
import { timeAgo } from '@/lib/utils'
import Link from 'next/link'
import {
@@ -11,55 +12,60 @@ import {
IconMoreVertical,
IconTrash2,
} from 'ui'
import ConfirmDeleteThreadModal from './ConfirmDeleteThreadModal'
import EditThreadModal from './EditThreadModal'
import { ThreadType } from './Threads'
import { useState } from 'react'
const Thread = ({
thread,
handleThreadActions,
onSelectEdit,
onSelectDelete,
}: {
thread: ThreadType
handleThreadActions: (formData: FormData) => void
onSelectEdit: () => void
onSelectDelete: () => void
}) => {
const Thread = ({ thread }: { thread: ThreadType }) => {
const formattedTimeAgo = timeAgo(thread.created_at)
//[Joshen] Just FYI Terry sorry i had to peel out your form component here which handled the delete
// Ideal UX for delete is to have a confirmation modal, and edit to be in a modal too so need client
// <form action={handleThreadActions} className="flex gap-2 items-center">
// <input type="hidden" name="threadID" value={thread.thread_id} />
// </form>
const [editOpen, setEditOpen] = useState(false)
const [deleteOpen, setDeleteOpen] = useState(false)
return (
<div
key={thread.id}
className="group flex items-center justify-between border rounded w-full px-4 py-2 transition bg-surface-100 hover:bg-surface-200"
>
<div className="flex flex-col gap-y-1">
<Link className="text-sm hover:underline" href={`/${thread.thread_id}/${thread.run_id}`}>
{thread.thread_title}
</Link>
<p className="text-xs text-foreground-light">Last updated {formattedTimeAgo}</p>
<>
<div
key={thread.id}
className="group flex items-center justify-between border rounded w-full px-4 py-2 transition bg-surface-100 hover:bg-surface-200"
>
<div className="flex flex-col gap-y-1">
<Link className="text-sm hover:underline" href={`/${thread.thread_id}/${thread.run_id}`}>
{thread.thread_title}
</Link>
<span className="text-xs text-foreground-light">Last updated {formattedTimeAgo}</span>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="text" icon={<IconMoreVertical />} className="px-1" />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-32" align="end">
<DropdownMenuItem className="space-x-2" onClick={() => setEditOpen(true)}>
<IconEdit2 size={14} />
<p>Edit name</p>
</DropdownMenuItem>
<DropdownMenuItem className="space-x-2" onClick={() => setDeleteOpen(true)}>
<IconTrash2 size={14} />
<p>Delete thread</p>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<DropdownMenu>
<DropdownMenuTrigger>
<Button type="text" icon={<IconMoreVertical />} className="px-1" />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-32" align="center">
<DropdownMenuItem className="space-x-2" onClick={onSelectEdit}>
<IconEdit2 size={14} />
<p>Edit name</p>
</DropdownMenuItem>
<DropdownMenuItem className="space-x-2" onClick={onSelectDelete}>
<IconTrash2 size={14} />
<p>Delete thread</p>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<ConfirmDeleteThreadModal
key={`${thread.id}-delete-dialog`}
thread={thread}
visible={deleteOpen}
onClose={() => setDeleteOpen(false)}
/>
<EditThreadModal
key={`${thread.id}-edit-dialog`}
thread={thread}
visible={editOpen}
onClose={() => setEditOpen(false)}
/>
</>
)
}
+21 -41
View File
@@ -1,56 +1,36 @@
'use client'
import { Database } from '@/types/supabase'
import EmptyState from './EmptyState'
import Thread from './Thread'
import { useState, useEffect } from 'react'
// import { useState, useEffect } from 'react'
import ConfirmDeleteThreadModal from './ConfirmDeleteThreadModal'
import EditThreadModal from './EditThreadModal'
import { cookies } from 'next/headers'
import { createClient } from '@/lib/supabase/server'
export type ThreadType = Database['public']['Tables']['threads']['Row']
interface ThreadsProps {
threads: ThreadType[]
handleThreadActions: (formData: FormData) => void
}
async function Threads() {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
const Threads = ({ threads, handleThreadActions }: ThreadsProps) => {
// To circumvent hydration errors, although not sure why its happening
const [mounted, setMounted] = useState(false)
const [selectedThreadToEdit, setSelectedThreadToEdit] = useState<ThreadType>()
const [selectedThreadToDelete, setSelectedThreadToDelete] = useState<ThreadType>()
const {
data: { user },
} = await supabase.auth.getUser()
useEffect(() => {
setMounted(true)
}, [])
if (!user) return <p>Error fetching user details</p>
const { data } = await supabase.from('threads').select().eq('user_id', user.id)
const threads = data ?? []
return (
mounted && (
<>
<div className="flex flex-col gap-y-3">
{threads.length > 0 ? (
threads.map((thread) => (
<Thread
key={thread.id}
thread={thread}
handleThreadActions={handleThreadActions}
onSelectEdit={() => setSelectedThreadToEdit(thread)}
onSelectDelete={() => setSelectedThreadToDelete(thread)}
/>
))
) : (
<EmptyState />
)}
</div>
<ConfirmDeleteThreadModal
thread={selectedThreadToDelete}
onClose={() => setSelectedThreadToDelete(undefined)}
/>
<EditThreadModal
thread={selectedThreadToEdit}
onClose={() => setSelectedThreadToEdit(undefined)}
/>
</>
)
<div className="flex flex-col gap-y-3">
{threads.length > 0 ? (
threads.map((thread) => <Thread key={`thread-item-${thread.id}`} thread={thread} />)
) : (
<EmptyState />
)}
</div>
)
}
+52 -39
View File
@@ -1,9 +1,9 @@
import { deleteThread } from '@/lib/actions'
import { createClient } from '@/lib/supabase/server'
import { default as dayjs, default as relativeTime } from 'dayjs'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import Threads from './Threads'
import { Suspense } from 'react'
dayjs.extend(relativeTime)
@@ -13,6 +13,43 @@ dayjs.extend(relativeTime)
// export const dynamic = 'force-dynamic'
const Profile = async () => {
// async function handleThreadActions(formData: FormData) {
// 'use server'
// const action = formData.get('action') as string
// const threadID = formData.get('threadID') as string
// if (!threadID) return
// if (action === 'delete') {
// deleteThread(threadID)
// }
// }
return (
<div className="grid grid-cols-4 gap-x-8 py-6 xl:py-12 gap-y-6 xl:gap-y-0">
<div className="col-span-4 xl:col-span-1 flex flex-col gap-y-6">
<div className="flex items-center gap-x-4">
<Suspense fallback={<p>Loading user...</p>}>
<ProfileDetails />
</Suspense>
</div>
</div>
<div className="col-span-4 xl:col-span-3 flex flex-col gap-y-4">
<p>Past conversations</p>
<div className="w-full h-px border-t" />
<Suspense fallback={<div>Loading...</div>}>
<Threads />
</Suspense>
</div>
</div>
)
}
async function ProfileDetails() {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
@@ -22,45 +59,21 @@ const Profile = async () => {
if (!user) redirect('/')
const { data } = await supabase.from('threads').select().eq('user_id', user.id)
const threads = data ?? []
async function handleThreadActions(formData: FormData) {
'use server'
const action = formData.get('action') as string
const threadID = formData.get('threadID') as string
if (!threadID) return
if (action === 'delete') {
deleteThread(threadID)
}
}
return (
<div className="grid grid-cols-4 gap-x-8 py-6 xl:py-12 gap-y-6 xl:gap-y-0">
<div className="col-span-4 xl:col-span-1 flex flex-col gap-y-6">
<div className="flex items-center gap-x-4">
<div
className="border border-foreground-lighter rounded-full w-12 h-12 bg-no-repeat bg-center bg-cover"
style={{ backgroundImage: `url('${user.user_metadata.avatar_url}')` }}
/>
<div className="flex flex-col">
<p className="text-lg">{user.user_metadata.full_name}</p>
<p className="text-foreground-light">@{user.user_metadata.user_name}</p>
</div>
</div>
return !user ? (
// make this better!
<p>No user found</p>
) : (
<>
<div
className="border border-foreground-lighter rounded-full w-12 h-12 bg-no-repeat bg-center bg-cover"
style={{ backgroundImage: `url('${user.user_metadata.avatar_url}')` }}
/>
<div className="flex flex-col">
<p className="text-lg">{user.user_metadata.full_name}</p>
<p className="text-foreground-light">@{user.user_metadata.user_name}</p>
</div>
<div className="col-span-4 xl:col-span-3 flex flex-col gap-y-4">
<p>Past conversations</p>
<div className="w-full h-px border-t" />
<Threads threads={threads} handleThreadActions={handleThreadActions} />
</div>
</div>
</>
)
}
export default Profile
+2 -2
View File
@@ -9,7 +9,7 @@ export const links = [
]
const Footer = () => (
<div className="border-t py-4 w-full px-4 flex justify-between">
<footer role="menu" className="border-t py-4 w-full px-4 flex justify-between">
<ul className="flex items-center gap-4 text-xs">
{links.map((link, index) => (
<li key={index}>
@@ -18,7 +18,7 @@ const Footer = () => (
))}
</ul>
<ThemeSwitcher />
</div>
</footer>
)
export default Footer
@@ -1,79 +1,55 @@
'use client'
import { HelpCircle, LogIn, LogOut } from 'lucide-react'
import { User } from '@supabase/supabase-js'
import Link from 'next/link'
import { HelpCircle, LogIn, LogOut } from 'lucide-react'
import Image from 'next/image'
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
IconUser,
SidePanel,
} from 'ui'
import Link from 'next/link'
import { Button, DropdownMenuGroup, DropdownMenuItem, DropdownMenuSeparator, IconUser } from 'ui'
import { logout } from './../../app/actions'
interface AvatarDropdownProps {
currentUser: User | null
signout: () => void
user: User
}
export default function AvatarDropdown({ currentUser, signout }: AvatarDropdownProps) {
export default function AvatarDropdown({ user }: AvatarDropdownProps) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild className="flex">
{currentUser ? (
<button
className="border border-foreground-lighter rounded-full w-[30px] h-[30px] bg-no-repeat bg-center bg-cover"
style={{ backgroundImage: `url('${currentUser.user_metadata.avatar_url}')` }}
/>
) : (
<Button type="outline" className="p-1.5 rounded-full" icon={<IconUser size={16} />} />
)}
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-48">
{currentUser ? (
<>
<div className="px-2 py-2">
<p className="text-xs text-foreground">{currentUser.user_metadata.full_name}</p>
<p className="text-xs text-foreground-light">{currentUser.user_metadata.email}</p>
</div>
<Link href="/profile">
<DropdownMenuItem className="space-x-2" onClick={() => {}}>
<IconUser size={14} strokeWidth={2} />
<p>Profile</p>
</DropdownMenuItem>
</Link>
<Link href="/faq">
<DropdownMenuItem className="space-x-2" onClick={() => {}}>
<HelpCircle size={14} />
<p>FAQs</p>
</DropdownMenuItem>
</Link>
<SidePanel.Separator />
<a href="https://supabase.com" target="_blank" rel="noreferrer">
<DropdownMenuItem className="space-x-2">
<Image alt="supabase" src="/supabase.png" width={14} height={14} />
<p>Supabase</p>
</DropdownMenuItem>
</a>
<DropdownMenuItem className="space-x-2" onClick={signout}>
<LogOut size={14} />
<p>Sign out</p>
</DropdownMenuItem>
</>
) : (
<div className="flex flex-col gap-y-2 p-2">
<div className="text-xs">
Sign in to <span className="text-foreground">database.design</span> to save your
conversations!
</div>
<Button type="default" icon={<LogIn size={14} />}>
<Link href="/login">Sign in</Link>
</Button>
</div>
)}
</DropdownMenuContent>
</DropdownMenu>
<>
<div className="px-2 py-2">
<p className="text-sm text-foreground">{user.user_metadata.full_name}</p>
<p className="text-sm text-foreground-light">{user.user_metadata.email}</p>
</div>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<Link href="/profile">
<DropdownMenuItem className="space-x-2" onClick={() => {}}>
<IconUser size={14} strokeWidth={1.5} className="text-lighter" />
<p>Profile</p>
</DropdownMenuItem>
</Link>
<Link href="/faq">
<DropdownMenuItem className="space-x-2" onClick={() => {}}>
<HelpCircle size={14} strokeWidth={1.5} className="text-lighter" />
<p>FAQs</p>
</DropdownMenuItem>
</Link>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<a href="https://supabase.com" target="_blank" rel="noreferrer">
<DropdownMenuItem className="space-x-2">
<Image alt="supabase" src="/supabase.png" width={14} height={14} />
<p>Supabase</p>
</DropdownMenuItem>
</a>
</DropdownMenuGroup>
<form action={logout}>
<DropdownMenuItem className="space-x-2 w-full" asChild>
<button type="submit">
<LogOut size={14} strokeWidth={1.5} className="text-lighter" />
<p>Sign out</p>
</button>
</DropdownMenuItem>
</form>
</>
)
}
@@ -3,6 +3,7 @@ import { createClient } from '@/lib/supabase/server'
import { cookies } from 'next/headers'
import CurrentThreadName from './CurrentThreadName'
import Link from 'next/link'
import UserDropdown from './UserDropdown'
const Header = async () => {
const cookieStore = cookies()
@@ -28,8 +29,10 @@ const Header = async () => {
<CurrentThreadName />
</div>
<HeaderActions user={user} />
<div className="flex gap-3">
<HeaderActions user={user} />
<UserDropdown />
</div>
</nav>
)
}
@@ -47,11 +47,11 @@ const HeaderActions = ({ user }: HeaderActionsProps) => {
<Link href="/">New conversation</Link>
</Button>
{currentUser ? (
{/* {currentUser ? (
<AvatarDropdown currentUser={currentUser} signout={signout} />
) : (
<NoUserDropdown />
)}
)} */}
</div>
)
}
@@ -1,48 +1,31 @@
import { LogIn } from 'lucide-react'
import Image from 'next/image'
import Link from 'next/link'
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
IconUser,
SidePanel,
} from 'ui'
import { DropdownMenuItem, DropdownMenuSeparator } from 'ui'
const NoUserDropdown = () => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild className="flex">
<Button
type="outline"
className="p-1.5 rounded-full"
icon={<IconUser size={16} strokeWidth={2} />}
/>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-48">
<div className="flex flex-col gap-y-2 p-2">
<div className="text-xs">
Sign in to <span className="text-foreground">database.design</span> to save your
conversations!
</div>
<>
<div className="flex flex-col gap-y-2 p-2">
<div className="text-sm">
Sign in to <span className="text-foreground">database.design</span> to save your database
designs!
</div>
<Link href="/login">
<DropdownMenuItem className="space-x-2">
<LogIn size={14} />
<p>Sign in</p>
</DropdownMenuItem>
</Link>
<SidePanel.Separator />
<a href="https://supabase.com" target="_blank" rel="noreferrer">
<DropdownMenuItem className="space-x-2">
<Image alt="supabase" src="/supabase.png" width={14} height={14} />
<p>Supabase</p>
</DropdownMenuItem>
</a>
</DropdownMenuContent>
</DropdownMenu>
</div>
<Link href="/login">
<DropdownMenuItem className="space-x-2">
<LogIn size={14} className="text-lighter" />
<p>Sign in</p>
</DropdownMenuItem>
</Link>
<DropdownMenuSeparator />
<a href="https://supabase.com" target="_blank" rel="noreferrer">
<DropdownMenuItem className="space-x-2">
<Image alt="supabase" src="/supabase.png" width={14} height={14} />
<p>Supabase</p>
</DropdownMenuItem>
</a>
</>
)
}
@@ -45,23 +45,19 @@ const SaveSchemaDropdown = () => {
}
return (
<DropdownMenu>
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button
type="default"
onClick={() => snap.setHideCode(!snap.hideCode)}
iconRight={<IconChevronDown />}
>
<Button type="default" iconRight={<IconChevronDown />}>
Save schema
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-44" align="center">
<DropdownMenuContent className="w-44" align="end">
<DropdownMenuItem className="space-x-2" onClick={() => copyToClipboard()}>
<IconClipboard size={14} strokeWidth={2} />
<IconClipboard size={14} strokeWidth={1.5} className="text-lighter" />
<p>Copy SQL</p>
</DropdownMenuItem>
<DropdownMenuItem className="space-x-2" onClick={() => downloadSQL()}>
<IconDownload size={14} strokeWidth={2} />
<IconDownload size={14} strokeWidth={1.5} className="text-lighter" />
<p>Download SQL</p>
</DropdownMenuItem>
<DropdownMenuItem className="space-x-2" onClick={() => loadSQLInSupabase()}>
@@ -0,0 +1,48 @@
import { createClient } from '@/lib/supabase/server'
import { User2 } from 'lucide-react'
import { cookies } from 'next/headers'
import { Suspense } from 'react'
import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from 'ui'
import AvatarDropdown from './AvatarDropdown'
import NoUserDropdown from './NoUserDropdown'
const EmptyUserButton = () => (
<Button type="outline" className="p-1.5 rounded-full" icon={<User2 size={16} />} />
)
export function UserDropdown() {
return (
<Suspense fallback={<EmptyUserButton />}>
<Dropdowns />
</Suspense>
)
}
async function Dropdowns() {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
const {
data: { user },
} = await supabase.auth.getUser()
return (
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild className="flex">
{user ? (
<button
className="border border-foreground-lighter rounded-full w-[30px] h-[30px] bg-no-repeat bg-center bg-cover"
style={{ backgroundImage: `url('${user.user_metadata.avatar_url}')` }}
/>
) : (
<EmptyUserButton />
)}
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-64">
{user ? <AvatarDropdown user={user} /> : <NoUserDropdown />}
</DropdownMenuContent>
</DropdownMenu>
)
}
export default UserDropdown
-23
View File
@@ -1,23 +0,0 @@
'use server'
import { revalidatePath } from 'next/cache'
import OpenAI from 'openai'
import { createClient } from './supabase/server'
import { cookies } from 'next/headers'
const openai = new OpenAI()
export async function deleteThread(threadID: string) {
'use server'
const cookieStore = cookies()
const supabase = createClient(cookieStore)
try {
await supabase.from('threads').delete().eq('thread_id', threadID)
} catch (error) {
if (error) console.error('Error deleting thread: ', error)
}
await openai.beta.threads.del(threadID)
revalidatePath('/profile')
}
+1 -1
View File
@@ -121,7 +121,7 @@ client_id = "env(GITHUB_CLIENT_ID)"
# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
secret = "env(GITHUB_CLIENT_SECRET)"
# Overrides the Github Authorization callback URL. Leave this empty if you've set the url in the Github oath app.
redirect_uri = "http://127.0.0.1:54321/auth/v1/callback"
redirect_uri = ""
# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
# or any other third-party OIDC providers.
url = ""
+2
View File
@@ -1,3 +1,5 @@
'use client'
import * as React from 'react'
// Providers
@@ -1,3 +1,5 @@
'use client'
import { Slot } from '@radix-ui/react-slot'
import { VariantProps, cva } from 'class-variance-authority'
import React from 'react'
@@ -1,3 +1,5 @@
'use client'
import React, { useEffect } from 'react'
// @ts-ignore
import ModalStyles from './Modal.module.css'