import React, { useEffect, useState, useMemo, useCallback } from 'react'; import { motion } from 'framer-motion'; import { toast } from 'react-toastify'; import { useStore } from '@nanostores/react'; import { logStore } from '~/lib/stores/logs'; import type { VercelUserResponse } from '~/types/vercel'; import { classNames } from '~/utils/classNames'; import { Button } from '~/components/ui/Button'; import { ServiceHeader, ConnectionTestIndicator } from '~/components/@settings/shared/service-integration'; import { useConnectionTest } from '~/lib/hooks'; import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '~/components/ui/Collapsible'; import Cookies from 'js-cookie'; import { vercelConnection, isConnecting, isFetchingStats, updateVercelConnection, fetchVercelStats, fetchVercelStatsViaAPI, initializeVercelConnection, } from '~/lib/stores/vercel'; interface ProjectAction { name: string; icon: string; action: (projectId: string) => Promise; requiresConfirmation?: boolean; variant?: 'default' | 'destructive' | 'outline'; } // Vercel logo SVG component const VercelLogo = () => ( ); export default function VercelTab() { const connection = useStore(vercelConnection); const connecting = useStore(isConnecting); const fetchingStats = useStore(isFetchingStats); const [isProjectsExpanded, setIsProjectsExpanded] = useState(false); const [isProjectActionLoading, setIsProjectActionLoading] = useState(false); // Use shared connection test hook const { testResult: connectionTest, testConnection, isTestingConnection, } = useConnectionTest({ testEndpoint: '/api/vercel-user', serviceName: 'Vercel', getUserIdentifier: (data: VercelUserResponse) => data.username || data.user?.username || data.email || data.user?.email || 'Vercel User', }); // Memoize project actions to prevent unnecessary re-renders const projectActions: ProjectAction[] = useMemo( () => [ { name: 'Redeploy', icon: 'i-ph:arrows-clockwise', action: async (projectId: string) => { try { const response = await fetch(`https://api.vercel.com/v1/deployments`, { method: 'POST', headers: { Authorization: `Bearer ${connection.token}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ name: projectId, target: 'production', }), }); if (!response.ok) { throw new Error('Failed to redeploy project'); } toast.success('Project redeployment initiated'); await fetchVercelStats(connection.token); } catch (err: unknown) { const error = err instanceof Error ? err.message : 'Unknown error'; toast.error(`Failed to redeploy project: ${error}`); } }, }, { name: 'View Dashboard', icon: 'i-ph:layout', action: async (projectId: string) => { window.open(`https://vercel.com/dashboard/${projectId}`, '_blank'); }, }, { name: 'View Deployments', icon: 'i-ph:rocket', action: async (projectId: string) => { window.open(`https://vercel.com/dashboard/${projectId}/deployments`, '_blank'); }, }, { name: 'View Functions', icon: 'i-ph:code', action: async (projectId: string) => { window.open(`https://vercel.com/dashboard/${projectId}/functions`, '_blank'); }, }, { name: 'View Analytics', icon: 'i-ph:chart-bar', action: async (projectId: string) => { const project = connection.stats?.projects.find((p) => p.id === projectId); if (project) { window.open(`https://vercel.com/${connection.user?.username}/${project.name}/analytics`, '_blank'); } }, }, { name: 'View Domains', icon: 'i-ph:globe', action: async (projectId: string) => { window.open(`https://vercel.com/dashboard/${projectId}/domains`, '_blank'); }, }, { name: 'View Settings', icon: 'i-ph:gear', action: async (projectId: string) => { window.open(`https://vercel.com/dashboard/${projectId}/settings`, '_blank'); }, }, { name: 'View Logs', icon: 'i-ph:scroll', action: async (projectId: string) => { window.open(`https://vercel.com/dashboard/${projectId}/logs`, '_blank'); }, }, { name: 'Delete Project', icon: 'i-ph:trash', action: async (projectId: string) => { try { const response = await fetch(`https://api.vercel.com/v1/projects/${projectId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${connection.token}`, }, }); if (!response.ok) { throw new Error('Failed to delete project'); } toast.success('Project deleted successfully'); await fetchVercelStats(connection.token); } catch (err: unknown) { const error = err instanceof Error ? err.message : 'Unknown error'; toast.error(`Failed to delete project: ${error}`); } }, requiresConfirmation: true, variant: 'destructive', }, ], [connection.token], ); // Only re-create when token changes // Initialize connection on component mount - check server-side token first useEffect(() => { const initializeConnection = async () => { try { // First try to initialize using server-side token await initializeVercelConnection(); // If no connection was established, the user will need to manually enter a token const currentState = vercelConnection.get(); if (!currentState.user) { console.log('No server-side Vercel token available, manual connection required'); } } catch (error) { console.error('Failed to initialize Vercel connection:', error); } }; initializeConnection(); }, []); useEffect(() => { const fetchProjects = async () => { if (connection.user) { // Use server-side API if we have a connected user try { await fetchVercelStatsViaAPI(connection.token); } catch { // Fallback to direct API if server-side fails and we have a token if (connection.token) { await fetchVercelStats(connection.token); } } } }; fetchProjects(); }, [connection.user, connection.token]); const handleConnect = async (event: React.FormEvent) => { event.preventDefault(); isConnecting.set(true); try { const token = connection.token; if (!token.trim()) { throw new Error('Token is required'); } // First test the token directly with Vercel API const testResponse = await fetch('https://api.vercel.com/v2/user', { headers: { Authorization: `Bearer ${token}`, 'User-Agent': 'bolt.diy-app', }, }); if (!testResponse.ok) { if (testResponse.status === 401) { throw new Error('Invalid Vercel token'); } throw new Error(`Vercel API error: ${testResponse.status}`); } const userData = (await testResponse.json()) as VercelUserResponse; // Set cookies for server-side API access Cookies.set('VITE_VERCEL_ACCESS_TOKEN', token, { expires: 365 }); // Normalize the user data structure const normalizedUser = userData.user || { id: userData.id || '', username: userData.username || '', email: userData.email || '', name: userData.name || '', avatar: userData.avatar, }; updateVercelConnection({ user: normalizedUser, token, }); await fetchVercelStats(token); toast.success('Successfully connected to Vercel'); } catch (error) { console.error('Auth error:', error); logStore.logError('Failed to authenticate with Vercel', { error }); const errorMessage = error instanceof Error ? error.message : 'Failed to connect to Vercel'; toast.error(errorMessage); updateVercelConnection({ user: null, token: '' }); } finally { isConnecting.set(false); } }; const handleDisconnect = () => { // Clear Vercel-related cookies Cookies.remove('VITE_VERCEL_ACCESS_TOKEN'); updateVercelConnection({ user: null, token: '' }); toast.success('Disconnected from Vercel'); }; const handleProjectAction = useCallback(async (projectId: string, action: ProjectAction) => { if (action.requiresConfirmation) { if (!confirm(`Are you sure you want to ${action.name.toLowerCase()}?`)) { return; } } setIsProjectActionLoading(true); await action.action(projectId); setIsProjectActionLoading(false); }, []); const renderProjects = useCallback(() => { if (fetchingStats) { return (
Fetching Vercel projects...
); } return (
Your Projects ({connection.stats?.totalProjects || 0})
{/* Vercel Overview Dashboard */} {connection.stats?.projects?.length ? (

Vercel Overview

{connection.stats.totalProjects}
Total Projects
{ connection.stats.projects.filter( (p) => p.targets?.production?.alias && p.targets.production.alias.length > 0, ).length }
Deployed Projects
{new Set(connection.stats.projects.map((p) => p.framework).filter(Boolean)).size}
Frameworks Used
{connection.stats.projects.filter((p) => p.latestDeployments?.[0]?.state === 'READY').length}
Active Deployments
) : null} {/* Performance Analytics */} {connection.stats?.projects?.length ? (

Performance Analytics

Deployment Health
{(() => { const totalDeployments = connection.stats.projects.reduce( (sum, p) => sum + (p.latestDeployments?.length || 0), 0, ); const readyDeployments = connection.stats.projects.filter( (p) => p.latestDeployments?.[0]?.state === 'READY', ).length; const errorDeployments = connection.stats.projects.filter( (p) => p.latestDeployments?.[0]?.state === 'ERROR', ).length; const successRate = totalDeployments > 0 ? Math.round((readyDeployments / connection.stats.projects.length) * 100) : 0; return [ { label: 'Success Rate', value: `${successRate}%` }, { label: 'Active', value: readyDeployments }, { label: 'Failed', value: errorDeployments }, ]; })().map((item, idx) => (
{item.label}: {item.value}
))}
Framework Distribution
{(() => { const frameworks = connection.stats.projects.reduce( (acc, p) => { if (p.framework) { acc[p.framework] = (acc[p.framework] || 0) + 1; } return acc; }, {} as Record, ); return Object.entries(frameworks) .sort(([, a], [, b]) => b - a) .slice(0, 3) .map(([framework, count]) => ({ label: framework, value: count })); })().map((item, idx) => (
{item.label}: {item.value}
))}
Activity Summary
{(() => { const now = Date.now(); const recentDeployments = connection.stats.projects.filter((p) => { const lastDeploy = p.latestDeployments?.[0]?.created; return lastDeploy && now - new Date(lastDeploy).getTime() < 7 * 24 * 60 * 60 * 1000; }).length; const totalDomains = connection.stats.projects.reduce( (sum, p) => sum + (p.targets?.production?.alias ? p.targets.production.alias.length : 0), 0, ); const avgDomainsPerProject = connection.stats.projects.length > 0 ? Math.round((totalDomains / connection.stats.projects.length) * 10) / 10 : 0; return [ { label: 'Recent deploys', value: recentDeployments }, { label: 'Total domains', value: totalDomains }, { label: 'Avg domains/project', value: avgDomainsPerProject }, ]; })().map((item, idx) => (
{item.label}: {item.value}
))}
) : null} {/* Project Health Overview */} {connection.stats?.projects?.length ? (

Project Health Overview

{(() => { const healthyProjects = connection.stats.projects.filter( (p) => p.latestDeployments?.[0]?.state === 'READY' && (p.targets?.production?.alias?.length ?? 0) > 0, ).length; const needsAttention = connection.stats.projects.filter( (p) => p.latestDeployments?.[0]?.state === 'ERROR' || p.latestDeployments?.[0]?.state === 'CANCELED', ).length; const withCustomDomain = connection.stats.projects.filter((p) => p.targets?.production?.alias?.some((alias: string) => !alias.includes('.vercel.app')), ).length; const buildingProjects = connection.stats.projects.filter( (p) => p.latestDeployments?.[0]?.state === 'BUILDING', ).length; return [ { label: 'Healthy', value: healthyProjects, icon: 'i-ph:check-circle', color: 'text-green-500', bgColor: 'bg-green-100 dark:bg-green-900/20', textColor: 'text-green-800 dark:text-green-400', }, { label: 'Custom Domain', value: withCustomDomain, icon: 'i-ph:globe', color: 'text-blue-500', bgColor: 'bg-blue-100 dark:bg-blue-900/20', textColor: 'text-blue-800 dark:text-blue-400', }, { label: 'Building', value: buildingProjects, icon: 'i-ph:gear', color: 'text-yellow-500', bgColor: 'bg-yellow-100 dark:bg-yellow-900/20', textColor: 'text-yellow-800 dark:text-yellow-400', }, { label: 'Issues', value: needsAttention, icon: 'i-ph:warning', color: 'text-red-500', bgColor: 'bg-red-100 dark:bg-red-900/20', textColor: 'text-red-800 dark:text-red-400', }, ]; })().map((metric, index) => (
{metric.label}
{metric.value}
))}
) : null} {connection.stats?.projects?.length ? (
{connection.stats.projects.map((project) => (
{project.name}
{project.targets?.production?.alias && project.targets.production.alias.length > 0 ? ( <> a.endsWith('.vercel.app') && !a.includes('-projects.vercel.app')) || project.targets.production.alias[0]}`} target="_blank" rel="noopener noreferrer" className="hover:text-bolt-elements-borderColorActive underline" > {project.targets.production.alias.find( (a: string) => a.endsWith('.vercel.app') && !a.includes('-projects.vercel.app'), ) || project.targets.production.alias[0]}
{new Date(project.createdAt).toLocaleDateString()} ) : project.latestDeployments && project.latestDeployments.length > 0 ? ( <> {project.latestDeployments[0].url}
{new Date(project.latestDeployments[0].created).toLocaleDateString()} ) : null}
{/* Project Details Grid */}
{/* Deployments - This would be fetched from API */} --
Deployments
{/* Domains - This would be fetched from API */} --
Domains
{/* Team Members - This would be fetched from API */} --
Team
{/* Bandwidth - This would be fetched from API */} --
Bandwidth
{project.latestDeployments && project.latestDeployments.length > 0 && (
{project.latestDeployments[0].state}
)} {project.framework && (
{project.framework}
)}
{projectActions.map((action) => ( ))}
))}
) : (
No projects found in your Vercel account
)}
); }, [ connection.stats, fetchingStats, isProjectsExpanded, isProjectActionLoading, handleProjectAction, projectActions, ]); console.log('connection', connection); return (
testConnection() : undefined} isTestingConnection={isTestingConnection} /> {/* Main Connection Component */}
{!connection.user ? (

Tip: You can also set the{' '} VITE_VERCEL_ACCESS_TOKEN {' '} environment variable to connect automatically.

updateVercelConnection({ ...connection, token: e.target.value })} disabled={connecting} placeholder="Enter your Vercel personal access token" className={classNames( 'w-full px-3 py-2 rounded-lg text-sm', 'bg-[#F8F8F8] dark:bg-[#1A1A1A]', 'border border-[#E5E5E5] dark:border-[#333333]', 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary', 'focus:outline-none focus:ring-1 focus:ring-bolt-elements-borderColorActive', 'disabled:opacity-50', )} />
) : (
Connected to Vercel
User Avatar

{connection.user?.username || 'Vercel User'}

{connection.user?.email || 'No email available'}

{connection.stats?.totalProjects || 0} Projects
{connection.stats?.projects.filter((p) => p.latestDeployments?.[0]?.state === 'READY').length || 0}{' '} Live
{/* Team size would be fetched from API */} --
{/* Usage Metrics */}
Projects
Active:{' '} {connection.stats?.projects.filter((p) => p.latestDeployments?.[0]?.state === 'READY').length || 0}
Total: {connection.stats?.totalProjects || 0}
Domains
{/* Domain usage would be fetched from API */}
Custom: --
Vercel: --
Usage
{/* Usage metrics would be fetched from API */}
Bandwidth: --
Requests: --
{renderProjects()}
)}
); }