From e23a1d4b6426df6ceb5e2fdb1cdbd79d1a89301f Mon Sep 17 00:00:00 2001 From: otdoges Date: Thu, 5 Jun 2025 17:00:07 -0500 Subject: [PATCH 01/13] Refactor layout and chat page components by removing the header with UserButton and simplifying the back button design. Update button styles for improved accessibility and user experience. Clean up unused code related to session ID display. --- app/chat/[id]/page.tsx | 33 +++++---------------------------- app/layout.tsx | 5 ----- 2 files changed, 5 insertions(+), 33 deletions(-) diff --git a/app/chat/[id]/page.tsx b/app/chat/[id]/page.tsx index 4cff0b05..b7a7908f 100644 --- a/app/chat/[id]/page.tsx +++ b/app/chat/[id]/page.tsx @@ -65,41 +65,18 @@ export default function ChatSessionPage() { > router.push("/")} - className="px-3 py-1 rounded-lg bg-white/5 hover:bg-white/10 transition-colors text-xs flex items-center gap-1" + className="p-2 rounded-full bg-white/5 hover:bg-white/10 transition-colors flex items-center justify-center" whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} + aria-label="Back to Home" > - - - + + + - Back to Home - {/* Session ID indicator */} - -
- Chat ID: {chatId} - -
-
- {/* Chat interface */} diff --git a/app/layout.tsx b/app/layout.tsx index ec0c903c..36194cbd 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -36,11 +36,6 @@ export default function RootLayout({ > -
- - - -
{children}
From 2dc604a56dd2b298b00880526ad1380dda9fb5a9 Mon Sep 17 00:00:00 2001 From: otdoges Date: Thu, 5 Jun 2025 17:06:33 -0500 Subject: [PATCH 02/13] Add SignedOut state to Home component with Sign In and Sign Up buttons --- app/page.tsx | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/app/page.tsx b/app/page.tsx index 9d419ffb..b68b2910 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -4,7 +4,7 @@ import { useRouter } from "next/navigation" import { motion } from "framer-motion"; import dynamic from 'next/dynamic'; import Hero from "@/components/hero"; -import { SignedIn, UserButton } from "@clerk/nextjs"; +import { SignedIn, SignedOut, UserButton } from "@clerk/nextjs"; import FinalCTA from "@/components/final-cta"; const FeaturesShowcase = dynamic(() => import('@/components/features-showcase'), { loading: () =>
}); @@ -38,6 +38,26 @@ export default function Home() {
+ +
+ router.push("/sign-in")} + className="px-4 py-2 rounded-lg bg-white/10 hover:bg-white/15 transition-all text-sm font-medium" + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + > + Sign In + + router.push("/sign-up")} + className="px-4 py-2 rounded-lg bg-gradient-to-r from-[#6C52A0] to-[#A0527C] hover:from-[#7C62B0] hover:to-[#B0627C] transition-all text-sm font-medium" + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + > + Sign Up + +
+
{/* Try it now button that navigates to chat */} From e4130fd10976acb079f505630134ee1ea83d05df Mon Sep 17 00:00:00 2001 From: otdoges Date: Thu, 5 Jun 2025 17:06:41 -0500 Subject: [PATCH 03/13] Update Home component to include SignedIn and SignedOut states with respective buttons for chat and sign-up navigation --- app/page.tsx | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index b68b2910..5eb299e3 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -67,18 +67,34 @@ export default function Home() { animate={{ opacity: 1, scale: 1 }} transition={{ delay: 1 }} > - router.push("/chat")} - className="px-6 py-3 rounded-full bg-gradient-to-r from-[#6C52A0] to-[#A0527C] hover:from-[#7C62B0] hover:to-[#B0627C] shadow-lg shadow-purple-900/20 flex items-center gap-2" - whileHover={{ scale: 1.05 }} - whileTap={{ scale: 0.95 }} - > - Try ZapDev Now - - - - - + + router.push("/chat")} + className="px-6 py-3 rounded-full bg-gradient-to-r from-[#6C52A0] to-[#A0527C] hover:from-[#7C62B0] hover:to-[#B0627C] shadow-lg shadow-purple-900/20 flex items-center gap-2" + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + > + Try ZapDev Now + + + + + + + + router.push("/sign-up")} + className="px-6 py-3 rounded-full bg-gradient-to-r from-[#6C52A0] to-[#A0527C] hover:from-[#7C62B0] hover:to-[#B0627C] shadow-lg shadow-purple-900/20 flex items-center gap-2" + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + > + Try ZapDev Now + + + + + + {/* Homepage sections */} From 81b30ff8065ea8628d6fbe7f496f925d4b943a8d Mon Sep 17 00:00:00 2001 From: otdoges Date: Thu, 5 Jun 2025 17:08:34 -0500 Subject: [PATCH 04/13] Refactor Home component to utilize SignInButton and SignUpButton from Clerk, replacing manual button implementations for improved functionality and user experience. --- app/page.tsx | 61 +++++++++++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 5eb299e3..14a3ce6e 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -4,7 +4,7 @@ import { useRouter } from "next/navigation" import { motion } from "framer-motion"; import dynamic from 'next/dynamic'; import Hero from "@/components/hero"; -import { SignedIn, SignedOut, UserButton } from "@clerk/nextjs"; +import { SignedIn, SignedOut, UserButton, SignInButton, SignUpButton } from "@clerk/nextjs"; import FinalCTA from "@/components/final-cta"; const FeaturesShowcase = dynamic(() => import('@/components/features-showcase'), { loading: () =>
}); @@ -40,22 +40,24 @@ export default function Home() {
- router.push("/sign-in")} - className="px-4 py-2 rounded-lg bg-white/10 hover:bg-white/15 transition-all text-sm font-medium" - whileHover={{ scale: 1.05 }} - whileTap={{ scale: 0.95 }} - > - Sign In - - router.push("/sign-up")} - className="px-4 py-2 rounded-lg bg-gradient-to-r from-[#6C52A0] to-[#A0527C] hover:from-[#7C62B0] hover:to-[#B0627C] transition-all text-sm font-medium" - whileHover={{ scale: 1.05 }} - whileTap={{ scale: 0.95 }} - > - Sign Up - + + + Sign In + + + + + Sign Up + +
@@ -82,18 +84,19 @@ export default function Home() { - router.push("/sign-up")} - className="px-6 py-3 rounded-full bg-gradient-to-r from-[#6C52A0] to-[#A0527C] hover:from-[#7C62B0] hover:to-[#B0627C] shadow-lg shadow-purple-900/20 flex items-center gap-2" - whileHover={{ scale: 1.05 }} - whileTap={{ scale: 0.95 }} - > - Try ZapDev Now - - - - - + + + Try ZapDev Now + + + + + + From b4898b0bb58fb824148c4e15f9da340e11eb211d Mon Sep 17 00:00:00 2001 From: otdoges Date: Thu, 5 Jun 2025 17:21:05 -0500 Subject: [PATCH 05/13] Add file attachment functionality to AnimatedAIChat component, allowing users to select and upload multiple files. Implement hidden file input and handle file changes to update attachments state. --- components/animated-ai-chat.tsx | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/components/animated-ai-chat.tsx b/components/animated-ai-chat.tsx index 558bbf97..d4c7e2a7 100644 --- a/components/animated-ai-chat.tsx +++ b/components/animated-ai-chat.tsx @@ -129,6 +129,7 @@ export function AnimatedAIChat({ chatId = "default" }: AnimatedAIChatProps) { const commandPaletteRef = useRef(null) const [messages, setMessages] = useState([]) const [isSplitScreen, setIsSplitScreen] = useState(false) + const fileInputRef = useRef(null) const commandSuggestions: CommandSuggestion[] = [ { @@ -296,8 +297,24 @@ export function AnimatedAIChat({ chatId = "default" }: AnimatedAIChatProps) { } const handleAttachFile = () => { - const mockFileName = `file-${Math.floor(Math.random() * 1000)}.pdf` - setAttachments((prev) => [...prev, mockFileName]) + // Trigger the hidden file input click event + if (fileInputRef.current) { + fileInputRef.current.click() + } + } + + const handleFileChange = (e: React.ChangeEvent) => { + const files = e.target.files + if (files && files.length > 0) { + // Add each selected file to attachments + Array.from(files).forEach(file => { + setAttachments(prev => [...prev, file.name]) + }) + } + // Reset the file input so the same file can be selected again + if (fileInputRef.current) { + fileInputRef.current.value = '' + } } const removeAttachment = (index: number) => { @@ -315,6 +332,15 @@ export function AnimatedAIChat({ chatId = "default" }: AnimatedAIChatProps) { return (
+ {/* Hidden file input */} + +
From bbb9d9e8544b2ddd7a13a4f279ca2ff6ed86e53c Mon Sep 17 00:00:00 2001 From: otdoges Date: Thu, 5 Jun 2025 18:52:46 -0500 Subject: [PATCH 06/13] Update .gitignore to include .env.local and add Convex and Svix dependencies in package.json and bun.lock. Refactor middleware to use basic clerkMiddleware without custom routes. Enhance PostHogProvider to conditionally initialize based on API key availability and log warnings in development. Update layout to include ConvexClientProvider and Navbar for improved structure. --- .gitignore | 2 + app/api/webhooks/clerk/route.ts | 128 +++++++++++++++++++++++++ app/auth-example/page.tsx | 116 +++++++++++++++++++++++ app/example/page.tsx | 66 +++++++++++++ app/layout.tsx | 25 ++--- bun.lock | 80 ++++++++++++++++ components/ConvexChatProvider.tsx | 123 ++++++++++++++++++++++++ components/ConvexClientProvider.tsx | 20 ++++ components/PostHogProvider.tsx | 39 ++++++-- components/ui/navbar.tsx | 45 +++++++++ convex/README.md | 90 ++++++++++++++++++ convex/_generated/api.d.ts | 44 +++++++++ convex/_generated/api.js | 22 +++++ convex/_generated/dataModel.d.ts | 60 ++++++++++++ convex/_generated/server.d.ts | 142 ++++++++++++++++++++++++++++ convex/_generated/server.js | 89 +++++++++++++++++ convex/auth.config.ts | 8 ++ convex/chats.ts | 115 ++++++++++++++++++++++ convex/clerk.ts | 88 +++++++++++++++++ convex/http.ts | 19 ++++ convex/messages.ts | 47 +++++++++ convex/schema.ts | 40 ++++++++ convex/tsconfig.json | 25 +++++ convex/users.ts | 65 +++++++++++++ lib/actions.ts | 49 ++++++++++ lib/convex.ts | 11 +++ lib/posthog.ts | 15 ++- middleware.ts | 27 +----- next.config.mjs | 20 +++- package.json | 2 + 30 files changed, 1574 insertions(+), 48 deletions(-) create mode 100644 app/api/webhooks/clerk/route.ts create mode 100644 app/auth-example/page.tsx create mode 100644 app/example/page.tsx create mode 100644 components/ConvexChatProvider.tsx create mode 100644 components/ConvexClientProvider.tsx create mode 100644 components/ui/navbar.tsx create mode 100644 convex/README.md create mode 100644 convex/_generated/api.d.ts create mode 100644 convex/_generated/api.js create mode 100644 convex/_generated/dataModel.d.ts create mode 100644 convex/_generated/server.d.ts create mode 100644 convex/_generated/server.js create mode 100644 convex/auth.config.ts create mode 100644 convex/chats.ts create mode 100644 convex/clerk.ts create mode 100644 convex/http.ts create mode 100644 convex/messages.ts create mode 100644 convex/schema.ts create mode 100644 convex/tsconfig.json create mode 100644 convex/users.ts create mode 100644 lib/actions.ts create mode 100644 lib/convex.ts diff --git a/.gitignore b/.gitignore index 7e261396..d4c991c6 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,5 @@ systemprompt-example.txt # Ehnhance Prompt example enhance-prompt.txt + +.env.local diff --git a/app/api/webhooks/clerk/route.ts b/app/api/webhooks/clerk/route.ts new file mode 100644 index 00000000..7be08d52 --- /dev/null +++ b/app/api/webhooks/clerk/route.ts @@ -0,0 +1,128 @@ +import { Webhook } from 'svix'; +import { headers } from 'next/headers'; +import { WebhookEvent } from '@clerk/nextjs/server'; +import { createClient } from '@supabase/supabase-js'; +import { NextResponse } from 'next/server'; + +// Initialize Supabase client +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; +const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; +const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY; + +// Helper function to create Supabase admin client with service role key +const getSupabaseAdmin = () => { + if (!supabaseUrl || !supabaseServiceKey) { + console.error('Missing Supabase environment variables for admin operations'); + return null; + } + return createClient(supabaseUrl, supabaseServiceKey); +}; + +export async function POST(req: Request) { + // Get the headers + const headerPayload = headers(); + const svix_id = headerPayload.get('svix-id'); + const svix_timestamp = headerPayload.get('svix-timestamp'); + const svix_signature = headerPayload.get('svix-signature'); + + // If there are no svix headers, error out + if (!svix_id || !svix_timestamp || !svix_signature) { + return new Response('Error: Missing svix headers', { + status: 400, + }); + } + + // Get the body + const payload = await req.json(); + const body = JSON.stringify(payload); + + // Create a new Svix instance with your webhook secret + const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET || ''); + + let evt: WebhookEvent; + + // Verify the payload with the headers + try { + evt = wh.verify(body, { + 'svix-id': svix_id, + 'svix-timestamp': svix_timestamp, + 'svix-signature': svix_signature, + }) as WebhookEvent; + } catch (err) { + console.error('Error verifying webhook:', err); + return new Response('Error verifying webhook', { + status: 400, + }); + } + + // Get the ID and type + const eventType = evt.type; + const supabase = getSupabaseAdmin(); + + if (!supabase) { + return NextResponse.json({ error: 'Could not initialize Supabase client' }, { status: 500 }); + } + + // Handle the event + try { + switch (eventType) { + case 'user.created': { + const { id, email_addresses, first_name, last_name, image_url } = evt.data; + const primaryEmail = email_addresses?.[0]?.email_address; + + // Create user in Supabase + const { error } = await supabase.from('users').insert({ + clerk_user_id: id, + email: primaryEmail, + first_name: first_name || null, + last_name: last_name || null, + avatar_url: image_url || null, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }); + + if (error) throw error; + break; + } + case 'user.updated': { + const { id, email_addresses, first_name, last_name, image_url } = evt.data; + const primaryEmail = email_addresses?.[0]?.email_address; + + // Update user in Supabase + const { error } = await supabase + .from('users') + .update({ + email: primaryEmail, + first_name: first_name || null, + last_name: last_name || null, + avatar_url: image_url || null, + updated_at: new Date().toISOString(), + }) + .eq('clerk_user_id', id); + + if (error) throw error; + break; + } + case 'user.deleted': { + const { id } = evt.data; + + // Delete user in Supabase + const { error } = await supabase + .from('users') + .delete() + .eq('clerk_user_id', id); + + if (error) throw error; + break; + } + } + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Error processing webhook:', error); + return NextResponse.json( + { error: 'Error processing webhook' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/auth-example/page.tsx b/app/auth-example/page.tsx new file mode 100644 index 00000000..1a6942cd --- /dev/null +++ b/app/auth-example/page.tsx @@ -0,0 +1,116 @@ +"use client"; + +import { Authenticated, Unauthenticated, AuthLoading, useConvexAuth } from "convex/react"; +import { SignInButton, UserButton } from "@clerk/nextjs"; +import { useQuery, useMutation } from "convex/react"; +import { api } from "@/convex/_generated/api"; +import { useState } from "react"; + +export default function AuthExamplePage() { + const { isAuthenticated, isLoading } = useConvexAuth(); + + return ( +
+

Convex + Clerk Authentication Example

+ + +
Loading authentication state...
+
+ + +
+
+ You are signed in: + +
+ + +
+
+ + +
+

You are not signed in.

+ + + +
+
+
+ ); +} + +function MessagesList() { + // This will only be called if the user is authenticated + const messages = useQuery(api.messages.list); + + return ( +
+

Your Messages

+ {messages === undefined ? ( +

Loading messages...

+ ) : messages.length === 0 ? ( +

No messages yet. Create your first message below!

+ ) : ( +
    + {messages.map((message) => ( +
  • +

    {message.content}

    +

    + {new Date(message.createdAt).toLocaleString()} +

    +
  • + ))} +
+ )} +
+ ); +} + +function MessageForm() { + const [content, setContent] = useState(""); + const createMessage = useMutation(api.messages.create); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!content.trim() || isSubmitting) return; + + setIsSubmitting(true); + try { + await createMessage({ content }); + setContent(""); + } catch (error) { + console.error("Failed to create message:", error); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+
+ +