diff --git a/.gitignore b/.gitignore index 20a7a476..09df3ea1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +**/.DS_Store + .env.local .env.*.local node_modules diff --git a/Dockerfile b/Dockerfile index e5ca70e1..1b5ac5b6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,30 @@ # Use an official Node runtime as the base image FROM node:18-alpine + +ARG NEXT_PUBLIC_SSO_DOMAIN +ARG NEXT_PUBLIC_SUPABASE_URL +ARG NEXT_PUBLIC_SUPABASE_ANON_KEY +ARG NEXT_PUBLIC_SITE_URL +ARG SUPABASE_SERVICE_ROLE_KEY +ARG SUPABASE_DATABASE_PASSWORD +ARG SUPABASE_JWT_SECRET +ARG SUPABASE_PROJECT_REF +ARG GITHUB_PROXY_CALLBACK_URL +ARG DIGGER_WEBHOOK_SECRET +ARG DIGGER_TRIGGER_APPLY_URL +ENV NEXT_PUBLIC_SSO_DOMAIN=$NEXT_PUBLIC_SSO_DOMAIN +ENV NEXT_PUBLIC_SUPABASE_URL=$NEXT_PUBLIC_SUPABASE_URL +ENV NEXT_PUBLIC_SUPABASE_ANON_KEY=$NEXT_PUBLIC_SUPABASE_ANON_KEY +ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL +ENV SUPABASE_SERVICE_ROLE_KEY=${SUPABASE_SERVICE_ROLE_KEY} +ENV SUPABASE_DATABASE_PASSWORD=${SUPABASE_DATABASE_PASSWORD} +ENV SUPABASE_JWT_SECRET=${SUPABASE_JWT_SECRET} +ENV SUPABASE_PROJECT_REF=${SUPABASE_PROJECT_REF} +ENV GITHUB_PROXY_CALLBACK_URL=${GITHUB_PROXY_CALLBACK_URL} +ENV DIGGER_WEBHOOK_SECRET=${DIGGER_WEBHOOK_SECRET} +ENV DIGGER_TRIGGER_APPLY_URL=${DIGGER_TRIGGER_APPLY_URL} + # Install pnpm RUN npm install -g pnpm diff --git a/README.md b/README.md index 5e5aff7a..b58a216a 100644 --- a/README.md +++ b/README.md @@ -1,9 +1 @@ -# Nextbase - -Nextbase Ultimate is a simple, fast, and secure way to build and deploy your next web application. It's built on top of [Next.js](https://nextjs.org/), [React](https://reactjs.org/), and [Supabase](https://supabase.com/). It is fully typed and uses [TypeScript](https://www.typescriptlang.org/), which means you can build your app with confidence. - -## Developing and deployment instructions - -Please checkout the documentation site [here](https://usenextbase.com/docs). - -Thanks! +# Digger next ui diff --git a/src/.DS_Store b/src/.DS_Store deleted file mode 100644 index acb79913..00000000 Binary files a/src/.DS_Store and /dev/null differ diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/chats/[chatId]/page.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/chats/[chatId]/page.tsx deleted file mode 100644 index a893898b..00000000 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/chats/[chatId]/page.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { ChatContainer } from "@/components/chat-container"; -import { getChatById } from "@/data/user/chats"; -import { getSlimProjectBySlug } from "@/data/user/projects"; - -import { nanoid, type Message } from "ai"; - - - -export default async function ChatPage({ params }: { params: { chatId: string; projectSlug: string } }) { - const { chatId, projectSlug } = params; - const project = await getSlimProjectBySlug(projectSlug); - - if (!chatId) { - const newChatId = nanoid(); - return ; - } - - const chat = await getChatById(chatId); - - if (chat.payload !== null && typeof chat.payload === "string") { - const { messages } = JSON.parse(chat.payload); - const assertedMessages = messages as unknown as Message[]; - return ( -
- -
- ); - } - - return ; -} diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/chats/page.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/chats/page.tsx deleted file mode 100644 index 0254a0a6..00000000 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/chats/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { ChatHistory } from "@/components/chat-history"; -import { getSlimProjectBySlug } from "@/data/user/projects"; - - -export default async function ChatsPage({ params }: { params: { projectSlug: string } }) { - const { projectSlug } = params; - const project = await getSlimProjectBySlug(projectSlug); - - - return ; -} - diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/image-generator/page.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/image-generator/page.tsx deleted file mode 100644 index 995882e0..00000000 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/image-generator/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { DallE } from "@/components/dall-e"; - -export default async function ImageGeneratorPage() { - - return ( -
- -
- ) -} - diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts deleted file mode 100644 index e7b689d2..00000000 --- a/src/app/api/chat/route.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { openai } from '@ai-sdk/openai'; -import { StreamingTextResponse, streamText } from 'ai'; - -export async function POST(req: Request) { - const { messages } = await req.json(); - - const result = await streamText({ - model: openai('gpt-3.5-turbo'), - messages, - }); - - return new StreamingTextResponse(result.toAIStream()); -} diff --git a/src/app/route.ts b/src/app/route.ts index e0bba0e2..5c685f58 100644 --- a/src/app/route.ts +++ b/src/app/route.ts @@ -30,6 +30,7 @@ export async function GET(request: NextRequest) { /* export async function GET() { + const supabase = createSupabaseUserRouteHandlerClient(); const { diff --git a/src/components/chat-container.tsx b/src/components/chat-container.tsx deleted file mode 100644 index cfec44b9..00000000 --- a/src/components/chat-container.tsx +++ /dev/null @@ -1,98 +0,0 @@ -'use client'; - -import { useChat, type Message } from 'ai/react'; - -import { insertChat } from '@/data/user/chats'; -import { useSAToastMutation } from '@/hooks/useSAToastMutation'; -import { cn } from '@/lib/utils'; -import { nanoid } from 'nanoid'; -import { usePathname } from 'next/navigation'; -import { toast } from 'sonner'; -import { ChatList } from './chat-list'; -import { ChatPanel } from './chat-panel'; -import { ChatScrollAnchor } from './chat-scroll-anchor'; -import { EmptyScreen } from './empty-screen'; - -export interface ChatProps extends React.ComponentProps<'div'> { - initialMessages?: Message[]; - id?: string; - project: { id: string, slug: string, name: string }; -} - - -export function ChatContainer({ id, initialMessages, className, project }: ChatProps) { - const { mutate } = useSAToastMutation(async ({ chatId, projectId, content }: { chatId: string, projectId: string, content: Message[] }) => { - return await insertChat(projectId, content, chatId); - }, { - errorMessage(error) { - try { - if (error instanceof Error) { - return String(error.message); - } - return `Failed to delete organization ${String(error)}`; - } catch (_err) { - console.warn(_err); - return 'Failed to delete organization'; - } - }, - }) - - const pathname = usePathname(); - - const { messages, append, reload, stop, isLoading, input, setInput } = - useChat({ - initialMessages, - id, - body: { - id, - }, - onFinish({ content }) { - messages.push({ - role: 'user', - content: input, - id: nanoid(), - }, { - role: 'assistant', - content, - id: nanoid(), - }); - - if (pathname === `/project/${project.slug}`) { - const chatPath = `/project/${project.slug}/chats/${id}`; - window.history.replaceState(null, '', chatPath); - } - mutate({ chatId: id ?? nanoid(), projectId: project.id, content: messages }); - }, - onResponse(response) { - if (response.status === 401) { - toast.error(response.statusText); - } - }, - }); - - return ( - <> -
- {messages.length ? ( - <> - - - - ) : ( - - )} -
- - - ); -} diff --git a/src/components/chat-history.tsx b/src/components/chat-history.tsx deleted file mode 100644 index d8a4d7b0..00000000 --- a/src/components/chat-history.tsx +++ /dev/null @@ -1,97 +0,0 @@ - -import Link from "next/link"; - -import { buttonVariants } from "@/components/ui/button"; - -import { cn } from "@/lib/utils"; -import { serverGetLoggedInUser } from "@/utils/server/serverGetLoggedInUser"; -import { PlusIcon } from "@radix-ui/react-icons"; - -import { getChats, getChatsHistory } from "@/data/user/chats"; -import { getSlimProjectById } from "@/data/user/projects"; -import type { Message } from "ai"; -import { formatRelative, subDays } from "date-fns"; -import { HomeIcon } from "lucide-react"; - -async function ChatList({ userId }: { userId: string }) { - const chats = await getChats(userId); - return ( -
- {chats.map((chat) => { - const { payload } = chat; - const messages = - typeof payload === "object" && - payload !== null && - "messages" in payload - ? payload.messages - : []; - const assertedMessages = messages as unknown as Message[]; - const title = - assertedMessages.length > 0 - ? assertedMessages[0].content - : "No title"; - - return ( - - {title} - - ); - })} -
- ); -} - -export async function ChatHistory({ projectId }: { projectId: string }) { - const user = await serverGetLoggedInUser(); - const project = await getSlimProjectById(projectId) - const userId = user.id; - const chatsHistory = await getChatsHistory(projectId, userId) - return ( -
-
- - - Home - -
-
- - - New Chat - -
- -
- {chatsHistory.map((chat, i) => ( - - Chat {chat.id} - {formatRelative(subDays(new Date(), 3), new Date(chat.created_at))} - - ))} -
- -
- ); -} diff --git a/src/components/chat-list.tsx b/src/components/chat-list.tsx deleted file mode 100644 index 1c3b3f20..00000000 --- a/src/components/chat-list.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import type { Message } from "ai"; - -import { Separator } from "@/components/ui/separator"; -import { ChatMessage } from "./chat-message"; -import { ScrollArea } from "./ui/scroll-area"; - -export interface ChatList { - messages: Message[]; -} - -export function ChatList({ messages }: ChatList) { - if (!messages.length) { - return null; - } - - return ( -
- - {messages.map((message, index) => ( -
- - {index < messages.length - 1 && ( - - )} -
- ))} -
-
- ); -} diff --git a/src/components/chat-message-actions.tsx b/src/components/chat-message-actions.tsx deleted file mode 100644 index 91eaa1f1..00000000 --- a/src/components/chat-message-actions.tsx +++ /dev/null @@ -1,40 +0,0 @@ -"use client"; - -import type { Message } from "ai"; - -import { Button } from "@/components/ui/button"; -import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; -import { cn } from "@/lib/utils"; -import { Check, Copy } from "lucide-react"; - -interface ChatMessageActionsProps extends React.ComponentProps<"div"> { - message: Message; -} - -export function ChatMessageActions({ - message, - className, - ...props -}: ChatMessageActionsProps) { - const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 }); - - const onCopy = () => { - if (isCopied) return; - copyToClipboard(message.content); - }; - - return ( -
- -
- ); -} diff --git a/src/components/chat-message.tsx b/src/components/chat-message.tsx deleted file mode 100644 index 6d90776f..00000000 --- a/src/components/chat-message.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import type { Message } from "ai"; -import remarkGfm from "remark-gfm"; -import remarkMath from "remark-math"; - -import { ChatMessageActions } from "@/components/chat-message-actions"; -import { MemoizedReactMarkdown } from "@/components/markdown"; -import { cn } from "@/lib/utils"; -import { Bot, User } from "lucide-react"; -import { CodeBlock } from "./ui/codeblock"; - - -export interface ChatMessageProps { - message: Message; -} - -export function ChatMessage({ message, ...props }: ChatMessageProps) { - return ( -
-
- {message.role === "user" ? : } -
-
- {children}

; - }, - // @ts-expect-error - `node` is not used - code({ node, inline, className, children, ...props }) { - const match = /language-(\w+)/.exec(className || ""); - - if (inline) { - return ( - - {children} - - ); - } - - return ( - - ); - }, - }} - > - {message.content} -
- -
-
- ); -} diff --git a/src/components/chat-panel.tsx b/src/components/chat-panel.tsx deleted file mode 100644 index 8f91517a..00000000 --- a/src/components/chat-panel.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import type { UseChatHelpers } from "ai/react"; - -import { PromptForm } from "@/components/prompt-form"; -import { Button } from "@/components/ui/button"; -import { RefreshCwIcon, StopCircle } from "lucide-react"; -import { ButtonScrollToBottom } from "./button-scroll-to-bottom"; - -export interface ChatPanelProps - extends Pick< - UseChatHelpers, - | "append" - | "isLoading" - | "reload" - | "messages" - | "stop" - | "input" - | "setInput" - > { - id?: string; - projectSlug: string; -} - -export function ChatPanel({ - id, - isLoading, - stop, - append, - reload, - input, - setInput, - messages, - projectSlug, -}: ChatPanelProps) { - return ( -
- -
-
- {isLoading ? ( - - ) : ( - messages?.length > 0 && ( - - ) - )} -
-
- { - await append({ - id, - content: value, - role: "user", - }); - }} - input={input} - setInput={setInput} - isLoading={isLoading} - /> -
-
-
- ); -} diff --git a/src/components/chat-scroll-anchor.tsx b/src/components/chat-scroll-anchor.tsx deleted file mode 100644 index 08ebc71c..00000000 --- a/src/components/chat-scroll-anchor.tsx +++ /dev/null @@ -1,28 +0,0 @@ -'use client'; - -import { useAtBottom } from '@/hooks/useAtBottom'; -import * as React from 'react'; -import { useInView } from 'react-intersection-observer'; - -interface ChatScrollAnchorProps { - trackVisibility?: boolean; -} - -export function ChatScrollAnchor({ trackVisibility }: ChatScrollAnchorProps) { - const isAtBottom = useAtBottom(); - const { ref, entry, inView } = useInView({ - trackVisibility, - delay: 100, - rootMargin: '0px 0px -150px 0px', - }); - - React.useEffect(() => { - if (isAtBottom && trackVisibility && !inView) { - entry?.target.scrollIntoView({ - block: 'start', - }); - } - }, [inView, entry, isAtBottom, trackVisibility]); - - return
; -} diff --git a/src/components/dall-e.tsx b/src/components/dall-e.tsx deleted file mode 100644 index 7f40171a..00000000 --- a/src/components/dall-e.tsx +++ /dev/null @@ -1,165 +0,0 @@ -"use client"; -import { convertAndUploadOpenAiImage } from "@/data/user/chats"; -import { updateUserProfileNameAndAvatar } from "@/data/user/user"; -import { useSAToastMutation } from "@/hooks/useSAToastMutation"; -import type { SAPayload } from "@/types"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation } from "@tanstack/react-query"; -import axios from "axios"; -import { CircleUserRound, Copy, Loader } from "lucide-react"; -import Image from "next/image"; -import { useState } from "react"; -import { Controller, useForm, type SubmitHandler } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; -import { Button } from "./ui/button"; -import { Input } from "./ui/input"; -import { Label } from "./ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "./ui/select"; -import { Skeleton } from "./ui/skeleton"; - - -type OpenAIImageList = { created: string; data: { b64_json: string }[] }; - -const generateImageSchema = z.object({ - prompt: z.string().min(1, { message: "Prompt is required" }), - size: z.string(), -}); - -export const DallE = () => { - const [images, setImages] = useState([]); - - const { mutate: updateProfilePictureMutation } = useSAToastMutation(async (data: { avatarUrl: string }) => { - return await updateUserProfileNameAndAvatar(data); - }, { - successMessage: "Profile picture updated successfully", - errorMessage: "Error updating profile picture" - }); - - const { mutate: generateImageMutation, isLoading } = useMutation( - async (data: { prompt: string; size: string }): Promise> => { - try { - const { data: response } = await axios.post("/api/generate-image", { - prompt: data.prompt, - size: data.size, - }); - return { - status: "success", - data: response as unknown as OpenAIImageList, - }; - } catch (error) { - return { - status: "error", - message: error.message, - }; - } - }, - { - onSuccess: async (response) => { - if (response.status === "success") { - const openAIResponse = response.data as unknown as OpenAIImageList; - const b64Data = openAIResponse.data[0].b64_json; - - try { - const image = await convertAndUploadOpenAiImage(b64Data); - if (image.status === "success" && image.data) { - setImages([image.data]); - } - } catch (error) { - console.error("Error uploading image:", error); - } - } - }, - }, - ); - - const onSubmit: SubmitHandler<{ - prompt: string; - size: string; - }> = (data) => { - generateImageMutation(data); - }; - - - const { register, handleSubmit, control, formState: { errors } } = useForm({ - defaultValues: { - prompt: "", - size: "512x512", - }, - resolver: zodResolver(generateImageSchema), - }); - - return ( -
- {errors.prompt &&
{errors.prompt.message}
} -
-
- - -
- -
- - ( - - )} - /> -
- -
- {!images.length &&
- Your images will be rendered here! -
} - {!isLoading ?
- {images.map((image) => ( -
-
- Generated Image -
- -
- - -
-
- ))} -
:
-
- - -

Generating...

-
-
-
} - - -
- ); -}; diff --git a/src/components/ui/codeblock.tsx b/src/components/ui/codeblock.tsx index 4cb0c24c..e05f8ca6 100644 --- a/src/components/ui/codeblock.tsx +++ b/src/components/ui/codeblock.tsx @@ -65,7 +65,7 @@ const CodeBlock: FC = memo(({ language, value }) => { 3, true, )}${fileExtension}`; - const fileName = window.prompt("Enter file name" || "", suggestedFileName); + const fileName = window.prompt("Enter file name", suggestedFileName); if (!fileName) { // User pressed cancel on prompt. diff --git a/src/data/user/chats.ts b/src/data/user/chats.ts deleted file mode 100644 index ee7c9c3b..00000000 --- a/src/data/user/chats.ts +++ /dev/null @@ -1,190 +0,0 @@ -'use server'; -import { supabaseAdminClient } from '@/supabase-clients/admin/supabaseAdminClient'; -import { createSupabaseUserServerActionClient } from '@/supabase-clients/user/createSupabaseUserServerActionClient'; -import { createSupabaseUserServerComponentClient } from '@/supabase-clients/user/createSupabaseUserServerComponentClient'; -import type { SAPayload, SupabaseFileUploadOptions, Table } from '@/types'; -import { serverGetLoggedInUser } from '@/utils/server/serverGetLoggedInUser'; -import type { Message } from 'ai'; -import { nanoid } from 'nanoid'; -import slugify from 'slugify'; -import urlJoin from 'url-join'; - -export const insertChat = async ( - projectId: string, - payload: Message[], - chatId: string, -): Promise>> => { - const supabase = createSupabaseUserServerActionClient(); - const user = await serverGetLoggedInUser(); - - const { data, error } = await supabase - .from('chats') - .upsert( - { - id: chatId, - project_id: projectId, - user_id: user.id, - payload: JSON.stringify({ messages: payload }), - }, - { onConflict: 'id' }, - ) - .select('*') - .single(); - - if (error) { - return { - status: 'error', - message: error.message, - }; - } - - return { - status: 'success', - data: data, - }; -}; - -export const getChatById = async (chatId: string): Promise> => { - const supabase = createSupabaseUserServerComponentClient(); - const { data, error } = await supabase - .from('chats') - .select('*') - .eq('id', chatId) - .single(); - - if (error) { - throw error; - } - - console.log; - - return data; -}; - -export const deleteChat = async (chatId: string): Promise => { - const supabase = createSupabaseUserServerActionClient(); - const { error } = await supabase.from('chats').delete().eq('id', chatId); - - if (error) { - throw error; - } -}; - -export const getChats = async (userId: string): Promise[]> => { - const supabase = createSupabaseUserServerComponentClient(); - const { data, error } = await supabase - .from('chats') - .select('*') - .eq('user_id', userId) - .order('created_at', { ascending: false }); - - if (error) { - throw error; - } - - return data; -}; - -export const getChatsHistory = async ( - projectId: string, - userId: string, -): Promise[]> => { - const supabase = createSupabaseUserServerComponentClient(); - const { data, error } = await supabase - .from('chats') - .select('*') - .eq('project_id', projectId) - .eq('user_id', userId); - - if (error) { - throw error; - } - - return data; -}; - -export const convertAndUploadOpenAiImage = async (b64_json: string): Promise> => { - const byteCharacters = atob(b64_json); - const byteNumbers = new Array(byteCharacters.length); - for (let i = 0; i < byteCharacters.length; i++) { - byteNumbers[i] = byteCharacters.charCodeAt(i); - } - const byteArray = new Uint8Array(byteNumbers); - - const file = new File([byteArray], nanoid(), { type: 'image/png' }); - const formData = new FormData(); - formData.append('file', file); - - const response = await uploadOpenAiImage(formData, file.name, { - upsert: true, - }); - - if (response.status === 'error') { - return { - status: 'error', - message: response.message, - }; - } - - - if (response.status === "success") { - return { - status: "success", - data: response.data, - }; - } - - return { - status: "error", - message: "Unknown error", - }; -}; - -export const uploadOpenAiImage = async ( - formData: FormData, - fileName: string, - fileOptions?: SupabaseFileUploadOptions | undefined, -): Promise> => { - "use server"; - const file = formData.get("file"); - if (!file) { - return { - status: "error", - message: "File is empty", - }; - } - const slugifiedFilename = slugify(fileName, { - lower: true, - strict: true, - replacement: "-", - }); - - const user = await serverGetLoggedInUser(); - const userId = user.id; - const userImagesPath = `${userId}/images/${slugifiedFilename}`; - - const { data, error } = await supabaseAdminClient.storage - .from("openai-images") - .upload(userImagesPath, file, fileOptions); - - if (error) { - return { - status: "error", - message: error.message, - }; - } - - const { path } = data; - - const filePath = path.split(",")[0]; - const supabaseFileUrl = urlJoin( - process.env.NEXT_PUBLIC_SUPABASE_URL, - "/storage/v1/object/public/openai-images", - filePath, - ); - - return { - status: "success", - data: supabaseFileUrl, - }; -}; diff --git a/src/middleware.ts b/src/middleware.ts index 56af38c3..3632b9d4 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -78,8 +78,11 @@ export async function middleware_NEXTBASE_LEGACY(req: NextRequest) { { supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL }, ); - if (req.nextUrl.pathname === '/') { - return res; + if ( + process.env.NEXT_PUBLIC_SSO_DOMAIN !== undefined && + req.nextUrl.pathname === '/' + ) { + return NextResponse.redirect(toSiteURL('/auth/sso-verify')); } const sessionResponse = await supabase.auth.getSession();