import React, { useState, useEffect } from 'react'; import { toast } from 'react-toastify'; import { classNames } from '~/utils/classNames'; import { useStore } from '@nanostores/react'; import { netlifyConnection, updateNetlifyConnection, initializeNetlifyConnection } from '~/lib/stores/netlify'; import type { NetlifySite, NetlifyDeploy, NetlifyBuild, NetlifyUser } from '~/types/netlify'; import { CloudIcon, BuildingLibraryIcon, ClockIcon, CodeBracketIcon, CheckCircleIcon, XCircleIcon, TrashIcon, ArrowPathIcon, LockClosedIcon, LockOpenIcon, RocketLaunchIcon, } from '@heroicons/react/24/outline'; import { Button } from '~/components/ui/Button'; import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '~/components/ui/Collapsible'; import { formatDistanceToNow } from 'date-fns'; import { Badge } from '~/components/ui/Badge'; // Add the Netlify logo SVG component at the top of the file const NetlifyLogo = () => ( ); // Add new interface for site actions interface SiteAction { name: string; icon: React.ComponentType; action: (siteId: string) => Promise; requiresConfirmation?: boolean; variant?: 'default' | 'destructive' | 'outline'; } export default function NetlifyConnection() { const connection = useStore(netlifyConnection); const [tokenInput, setTokenInput] = useState(''); const [fetchingStats, setFetchingStats] = useState(false); const [sites, setSites] = useState([]); const [deploys, setDeploys] = useState([]); const [builds, setBuilds] = useState([]); const [deploymentCount, setDeploymentCount] = useState(0); const [lastUpdated, setLastUpdated] = useState(''); const [isStatsOpen, setIsStatsOpen] = useState(false); const [activeSiteIndex, setActiveSiteIndex] = useState(0); const [isActionLoading, setIsActionLoading] = useState(false); const [isConnecting, setIsConnecting] = useState(false); // Add site actions const siteActions: SiteAction[] = [ { name: 'Clear Cache', icon: ArrowPathIcon, action: async (siteId: string) => { try { const response = await fetch(`https://api.netlify.com/api/v1/sites/${siteId}/cache`, { method: 'POST', headers: { Authorization: `Bearer ${connection.token}`, }, }); if (!response.ok) { throw new Error('Failed to clear cache'); } toast.success('Site cache cleared successfully'); } catch (err: unknown) { const error = err instanceof Error ? err.message : 'Unknown error'; toast.error(`Failed to clear site cache: ${error}`); } }, }, { name: 'Delete Site', icon: TrashIcon, action: async (siteId: string) => { try { const response = await fetch(`https://api.netlify.com/api/v1/sites/${siteId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${connection.token}`, }, }); if (!response.ok) { throw new Error('Failed to delete site'); } toast.success('Site deleted successfully'); fetchNetlifyStats(connection.token); } catch (err: unknown) { const error = err instanceof Error ? err.message : 'Unknown error'; toast.error(`Failed to delete site: ${error}`); } }, requiresConfirmation: true, variant: 'destructive', }, ]; // Add deploy management functions const handleDeploy = async (siteId: string, deployId: string, action: 'lock' | 'unlock' | 'publish') => { try { setIsActionLoading(true); const endpoint = action === 'publish' ? `https://api.netlify.com/api/v1/sites/${siteId}/deploys/${deployId}/restore` : `https://api.netlify.com/api/v1/deploys/${deployId}/${action}`; const response = await fetch(endpoint, { method: 'POST', headers: { Authorization: `Bearer ${connection.token}`, }, }); if (!response.ok) { throw new Error(`Failed to ${action} deploy`); } toast.success(`Deploy ${action}ed successfully`); fetchNetlifyStats(connection.token); } catch (err: unknown) { const error = err instanceof Error ? err.message : 'Unknown error'; toast.error(`Failed to ${action} deploy: ${error}`); } finally { setIsActionLoading(false); } }; useEffect(() => { // Initialize connection with environment token if available initializeNetlifyConnection(); }, []); useEffect(() => { // Check if we have a connection with a token but no stats if (connection.user && connection.token && (!connection.stats || !connection.stats.sites)) { fetchNetlifyStats(connection.token); } // Update local state from connection if (connection.stats) { setSites(connection.stats.sites || []); setDeploys(connection.stats.deploys || []); setBuilds(connection.stats.builds || []); setDeploymentCount(connection.stats.deploys?.length || 0); setLastUpdated(connection.stats.lastDeployTime || ''); } }, [connection]); const handleConnect = async () => { if (!tokenInput) { toast.error('Please enter a Netlify API token'); return; } setIsConnecting(true); try { const response = await fetch('https://api.netlify.com/api/v1/user', { headers: { Authorization: `Bearer ${tokenInput}`, }, }); if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } const userData = (await response.json()) as NetlifyUser; // Update the connection store updateNetlifyConnection({ user: userData, token: tokenInput, }); toast.success('Connected to Netlify successfully'); // Fetch stats after successful connection fetchNetlifyStats(tokenInput); } catch (error) { console.error('Error connecting to Netlify:', error); toast.error(`Failed to connect to Netlify: ${error instanceof Error ? error.message : 'Unknown error'}`); } finally { setIsConnecting(false); setTokenInput(''); } }; const handleDisconnect = () => { // Clear from localStorage localStorage.removeItem('netlify_connection'); // Remove cookies document.cookie = 'netlifyToken=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; // Update the store updateNetlifyConnection({ user: null, token: '' }); toast.success('Disconnected from Netlify'); }; const fetchNetlifyStats = async (token: string) => { setFetchingStats(true); try { // Fetch sites const sitesResponse = await fetch('https://api.netlify.com/api/v1/sites', { headers: { Authorization: `Bearer ${token}`, }, }); if (!sitesResponse.ok) { throw new Error(`Failed to fetch sites: ${sitesResponse.statusText}`); } const sitesData = (await sitesResponse.json()) as NetlifySite[]; setSites(sitesData); // Fetch recent deploys for the first site (if any) let deploysData: NetlifyDeploy[] = []; let buildsData: NetlifyBuild[] = []; let lastDeployTime = ''; if (sitesData && sitesData.length > 0) { const firstSite = sitesData[0]; // Fetch deploys const deploysResponse = await fetch(`https://api.netlify.com/api/v1/sites/${firstSite.id}/deploys`, { headers: { Authorization: `Bearer ${token}`, }, }); if (deploysResponse.ok) { deploysData = (await deploysResponse.json()) as NetlifyDeploy[]; setDeploys(deploysData); setDeploymentCount(deploysData.length); // Get the latest deploy time if (deploysData.length > 0) { lastDeployTime = deploysData[0].created_at; setLastUpdated(lastDeployTime); // Fetch builds for the site const buildsResponse = await fetch(`https://api.netlify.com/api/v1/sites/${firstSite.id}/builds`, { headers: { Authorization: `Bearer ${token}`, }, }); if (buildsResponse.ok) { buildsData = (await buildsResponse.json()) as NetlifyBuild[]; setBuilds(buildsData); } } } } // Update the stats in the store updateNetlifyConnection({ stats: { sites: sitesData, deploys: deploysData, builds: buildsData, lastDeployTime, totalSites: sitesData.length, }, }); toast.success('Netlify stats updated'); } catch (error) { console.error('Error fetching Netlify stats:', error); toast.error(`Failed to fetch Netlify stats: ${error instanceof Error ? error.message : 'Unknown error'}`); } finally { setFetchingStats(false); } }; const renderStats = () => { if (!connection.user || !connection.stats) { return null; } return (
Netlify Stats
{connection.stats.totalSites} Sites {deploymentCount} Deployments {lastUpdated && ( Updated {formatDistanceToNow(new Date(lastUpdated))} ago )}
{sites.length > 0 && (

Your Sites

{sites.map((site, index) => (
{ setActiveSiteIndex(index); }} >
{site.name}
{site.published_deploy?.state === 'ready' ? ( ) : ( )} {site.published_deploy?.state || 'Unknown'}
{activeSiteIndex === index && ( <>
{siteActions.map((action) => ( ))}
{site.published_deploy && (
Published {formatDistanceToNow(new Date(site.published_deploy.published_at))} ago
{site.published_deploy.branch && (
Branch: {site.published_deploy.branch}
)}
)} )}
))}
{activeSiteIndex !== -1 && deploys.length > 0 && (

Recent Deployments

{deploys.map((deploy) => (
{deploy.state === 'ready' ? ( ) : deploy.state === 'error' ? ( ) : ( )} {deploy.state}
{formatDistanceToNow(new Date(deploy.created_at))} ago
{deploy.branch && (
Branch: {deploy.branch}
)} {deploy.deploy_url && ( )}
{deploy.state === 'ready' ? ( ) : ( )}
))}
)} {activeSiteIndex !== -1 && builds.length > 0 && (

Recent Builds

{builds.map((build) => (
{build.done && !build.error ? ( ) : build.error ? ( ) : ( )} {build.done ? (build.error ? 'Failed' : 'Completed') : 'In Progress'}
{formatDistanceToNow(new Date(build.created_at))} ago
{build.error && (
Error: {build.error}
)}
))}
)}
)}
); }; return (

Netlify Connection

{!connection.user ? (
setTokenInput(e.target.value)} placeholder="Enter your Netlify API token" className={classNames( 'w-full px-3 py-2 rounded-lg text-sm', 'bg-bolt-elements-background-depth-1 dark:bg-bolt-elements-background-depth-1', 'border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor', 'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary dark:placeholder-bolt-elements-textTertiary', 'focus:outline-none focus:ring-1 focus:ring-bolt-elements-item-contentAccent dark:focus:ring-bolt-elements-item-contentAccent', )} />
Get your token
) : (
Connected to Netlify
{renderStats()}
)}
); }