mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
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]'
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in new issue
Block a user