feat: add Vercel integration for project deployment

This commit introduces Vercel integration, enabling users to deploy projects directly to Vercel. It includes:
- New Vercel types and store for managing connections and stats.
- A VercelConnection component for managing Vercel account connections.
- A VercelDeploymentLink component for displaying deployment links.
- API routes for handling Vercel deployments.
- Updates to the HeaderActionButtons component to support Vercel deployment.

The integration allows users to connect their Vercel accounts, view project stats, and deploy projects with ease.
This commit is contained in:
KevIsDev
2025-03-27 00:06:10 +00:00
parent 1364d4a503
commit 687b03ba74
9 changed files with 982 additions and 20 deletions

View File

@@ -30,10 +30,10 @@ export function NetlifyDeploymentLink() {
rel="noopener noreferrer"
className="inline-flex items-center justify-center w-8 h-8 rounded hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-textSecondary hover:text-[#00AD9F] z-50"
onClick={(e) => {
e.stopPropagation(); // Add this to prevent click from bubbling up
e.stopPropagation(); // This is to prevent click from bubbling up
}}
>
<div className="i-ph:rocket-launch w-5 h-5" />
<div className="i-ph:link w-4 h-4 hover:text-blue-400" />
</a>
</Tooltip.Trigger>
<Tooltip.Portal>

View File

@@ -0,0 +1,158 @@
import { useStore } from '@nanostores/react';
import { vercelConnection } from '~/lib/stores/vercel';
import { chatId } from '~/lib/persistence/useChatHistory';
import * as Tooltip from '@radix-ui/react-tooltip';
import { useEffect, useState } from 'react';
export function VercelDeploymentLink() {
const connection = useStore(vercelConnection);
const currentChatId = useStore(chatId);
const [deploymentUrl, setDeploymentUrl] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
async function fetchProjectData() {
if (!connection.token || !currentChatId) {
return;
}
// Check if we have a stored project ID for this chat
const projectId = localStorage.getItem(`vercel-project-${currentChatId}`);
if (!projectId) {
return;
}
setIsLoading(true);
try {
// Fetch projects directly from the API
const projectsResponse = await fetch('https://api.vercel.com/v9/projects', {
headers: {
Authorization: `Bearer ${connection.token}`,
'Content-Type': 'application/json',
},
cache: 'no-store',
});
if (!projectsResponse.ok) {
throw new Error(`Failed to fetch projects: ${projectsResponse.status}`);
}
const projectsData = (await projectsResponse.json()) as any;
const projects = projectsData.projects || [];
// Extract the chat number from currentChatId
const chatNumber = currentChatId.split('-')[0];
// Find project by matching the chat number in the name
const project = projects.find((p: { name: string | string[] }) => p.name.includes(`bolt-diy-${chatNumber}`));
if (project) {
// Fetch project details including deployments
const projectDetailsResponse = await fetch(`https://api.vercel.com/v9/projects/${project.id}`, {
headers: {
Authorization: `Bearer ${connection.token}`,
'Content-Type': 'application/json',
},
cache: 'no-store',
});
if (projectDetailsResponse.ok) {
const projectDetails = (await projectDetailsResponse.json()) as any;
// Try to get URL from production aliases first
if (projectDetails.targets?.production?.alias && projectDetails.targets.production.alias.length > 0) {
// Find the clean URL (without -projects.vercel.app)
const cleanUrl = projectDetails.targets.production.alias.find(
(a: string) => a.endsWith('.vercel.app') && !a.includes('-projects.vercel.app'),
);
if (cleanUrl) {
setDeploymentUrl(`https://${cleanUrl}`);
return;
} else {
// If no clean URL found, use the first alias
setDeploymentUrl(`https://${projectDetails.targets.production.alias[0]}`);
return;
}
}
}
// If no aliases or project details failed, try fetching deployments
const deploymentsResponse = await fetch(
`https://api.vercel.com/v6/deployments?projectId=${project.id}&limit=1`,
{
headers: {
Authorization: `Bearer ${connection.token}`,
'Content-Type': 'application/json',
},
cache: 'no-store',
},
);
if (deploymentsResponse.ok) {
const deploymentsData = (await deploymentsResponse.json()) as any;
if (deploymentsData.deployments && deploymentsData.deployments.length > 0) {
setDeploymentUrl(`https://${deploymentsData.deployments[0].url}`);
return;
}
}
}
// Fallback to API call if not found in fetched projects
const fallbackResponse = await fetch(`/api/vercel-deploy?projectId=${projectId}&token=${connection.token}`, {
method: 'GET',
});
const data = await fallbackResponse.json();
if ((data as { deploy?: { url?: string } }).deploy?.url) {
setDeploymentUrl((data as { deploy: { url: string } }).deploy.url);
} else if ((data as { project?: { url?: string } }).project?.url) {
setDeploymentUrl((data as { project: { url: string } }).project.url);
}
} catch (err) {
console.error('Error fetching Vercel deployment:', err);
} finally {
setIsLoading(false);
}
}
fetchProjectData();
}, [connection.token, currentChatId]);
if (!deploymentUrl) {
return null;
}
return (
<Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<a
href={deploymentUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center w-8 h-8 rounded hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-textSecondary hover:text-[#000000] z-50"
onClick={(e) => {
e.stopPropagation();
}}
>
<div className={`i-ph:link w-4 h-4 hover:text-blue-400 ${isLoading ? 'animate-pulse' : ''}`} />
</a>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content
className="px-3 py-2 rounded bg-bolt-elements-background-depth-3 text-bolt-elements-textPrimary text-xs z-50"
sideOffset={5}
>
{deploymentUrl}
<Tooltip.Arrow className="fill-bolt-elements-background-depth-3" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
);
}