Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ DEBUG=
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
CLERK_SECRET_KEY=
CLERK_JWT_ISSUER_DOMAIN=
CLERK_WEBHOOK_SECRET=

# Convex configuration
NEXT_PUBLIC_CONVEX_URL=
Expand Down
100 changes: 100 additions & 0 deletions app/api/webhooks/clerk/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { Webhook } from 'svix';
import { headers } from 'next/headers';
import { WebhookEvent } from '@clerk/nextjs/server';
import { api } from '@/convex/_generated/api';
import { ConvexHttpClient } from 'convex/browser';

const convex = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!);

export async function POST(req: Request) {
// Get the Svix headers for webhook verification
const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET;

if (!WEBHOOK_SECRET) {
throw new Error('Please add CLERK_WEBHOOK_SECRET from Clerk Dashboard to .env or .env.local');
}

// Get the headers
const headerPayload = await 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 headers, error out
if (!svix_id || !svix_timestamp || !svix_signature) {
return new Response('Error occurred -- no svix headers', {
status: 400,
});
}

// Get the body
const payload = await req.json();
const body = JSON.stringify(payload);

// Create a new Svix instance with your secret.
const wh = new Webhook(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 occurred', {
status: 400,
});
}

// Handle the webhook
const eventType = evt.type;

if (eventType === 'user.created' || eventType === 'user.updated') {
const { id, email_addresses, first_name, last_name, image_url, username } = evt.data;

// Get the primary email address
const primaryEmail = email_addresses.find((email) => email.id === evt.data.primary_email_address_id);

if (!primaryEmail) {
return new Response('No primary email found', { status: 400 });
}

try {
// Sync user to Convex database
await convex.mutation(api.users.syncUser, {
userId: id,
email: primaryEmail.email_address,
fullName: first_name && last_name ? `${first_name} ${last_name}` : undefined,
avatarUrl: image_url,
username: username ?? undefined,
});

console.log(`User ${eventType === 'user.created' ? 'created' : 'updated'}:`, id);
} catch (error) {
console.error(`Error syncing user to Convex:`, error);
return new Response('Error syncing user', { status: 500 });
}
}

if (eventType === 'user.deleted') {
const { id } = evt.data;

try {
// Delete user from Convex database
await convex.mutation(api.users.deleteUser, {
userId: id!,
});

console.log('User deleted:', id);
} catch (error) {
console.error('Error deleting user from Convex:', error);
return new Response('Error deleting user', { status: 500 });
}
}

return new Response('Webhook processed successfully', { status: 200 });
}
20 changes: 13 additions & 7 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import HeaderWrapper from "@/components/shared/header/Wrapper/Wrapper";
import HeaderDropdownWrapper from "@/components/shared/header/Dropdown/Wrapper/Wrapper";
import GithubIcon from "@/components/shared/header/Github/_svg/GithubIcon";
import ButtonUI from "@/components/ui/shadcn/button"
import { SignInButton, SignUpButton, SignedIn, SignedOut, UserButton } from "@clerk/nextjs"
import { SignInButton, SignUpButton, SignedIn, SignedOut, UserButton, useAuth } from "@clerk/nextjs"

interface SearchResult {
url: string;
Expand All @@ -38,6 +38,7 @@ interface SearchResult {
}

export default function HomePage() {
const { isSignedIn } = useAuth();
const [url, setUrl] = useState<string>("");
const [selectedStyle, setSelectedStyle] = useState<string>("1");
const [selectedModel, setSelectedModel] = useState<string>(appConfig.ai.defaultModel);
Expand Down Expand Up @@ -83,8 +84,13 @@ export default function HomePage() {
}));

const handleSubmit = async (selectedResult?: SearchResult) => {
if (!isSignedIn) {
toast.error("Please sign in to use ZapDev");
return;
}

const inputValue = url.trim();

if (!inputValue) {
toast.error("Please enter a URL or search term");
return;
Expand Down Expand Up @@ -399,9 +405,9 @@ export default function HomePage() {
key={style.id}
onClick={() => setSelectedStyle(style.id)}
className={`
py-2.5 px-2 rounded text-[10px] font-medium border transition-all text-center
${selectedStyle === style.id
? 'border-orange-500 bg-orange-50 text-orange-900'
py-2.5 px-2 rounded-md text-[10px] font-medium border transition-all text-center
${selectedStyle === style.id
? 'border-orange-500 bg-orange-50 text-orange-900'
: 'border-gray-200 hover:border-gray-300 bg-white text-gray-700'
}
${isValidUrl ? 'opacity-100' : 'opacity-0'}
Expand All @@ -425,7 +431,7 @@ export default function HomePage() {
<select
value={selectedModel}
onChange={(e) => setSelectedModel(e.target.value)}
className="px-3 py-2.5 text-[10px] font-medium text-gray-700 bg-white rounded border border-gray-200 focus:border-orange-500 focus:outline-none focus:ring-1 focus:ring-orange-500"
className="px-3 py-2.5 text-[10px] font-medium text-gray-700 bg-white rounded-md border border-gray-200 focus:border-orange-500 focus:outline-none focus:ring-1 focus:ring-orange-500"
>
{models.map((model) => (
<option key={model.id} value={model.id}>
Expand All @@ -437,7 +443,7 @@ export default function HomePage() {
{/* Additional Instructions */}
<input
type="text"
className="flex-1 px-3 py-2.5 text-[10px] text-gray-700 bg-gray-50 rounded border border-gray-200 focus:border-orange-500 focus:outline-none focus:ring-1 focus:ring-orange-500 placeholder:text-gray-400"
className="flex-1 px-3 py-2.5 text-[10px] text-gray-700 bg-gray-50 rounded-md border border-gray-200 focus:border-orange-500 focus:outline-none focus:ring-1 focus:ring-orange-500 placeholder:text-gray-400"
placeholder="Additional instructions (optional)"
onChange={(e) => sessionStorage.setItem('additionalInstructions', e.target.value)}
/>
Expand Down
Loading