Spaces:
Running
Running
import React, { useEffect, useState } from 'react'; | |
import { motion } from 'framer-motion'; | |
import { toast } from 'react-toastify'; | |
import { useStore } from '@nanostores/react'; | |
import { logStore } from '~/lib/stores/logs'; | |
import { classNames } from '~/utils/classNames'; | |
import { | |
netlifyConnection, | |
isConnecting, | |
isFetchingStats, | |
updateNetlifyConnection, | |
fetchNetlifyStats, | |
} from '~/lib/stores/netlify'; | |
import type { NetlifyUser } from '~/types/netlify'; | |
export function NetlifyConnection() { | |
const connection = useStore(netlifyConnection); | |
const connecting = useStore(isConnecting); | |
const fetchingStats = useStore(isFetchingStats); | |
const [isSitesExpanded, setIsSitesExpanded] = useState(false); | |
useEffect(() => { | |
const fetchSites = async () => { | |
if (connection.user && connection.token) { | |
await fetchNetlifyStats(connection.token); | |
} | |
}; | |
fetchSites(); | |
}, [connection.user, connection.token]); | |
const handleConnect = async (event: React.FormEvent) => { | |
event.preventDefault(); | |
isConnecting.set(true); | |
try { | |
const response = await fetch('https://api.netlify.com/api/v1/user', { | |
headers: { | |
Authorization: `Bearer ${connection.token}`, | |
'Content-Type': 'application/json', | |
}, | |
}); | |
if (!response.ok) { | |
throw new Error('Invalid token or unauthorized'); | |
} | |
const userData = (await response.json()) as NetlifyUser; | |
updateNetlifyConnection({ | |
user: userData, | |
token: connection.token, | |
}); | |
await fetchNetlifyStats(connection.token); | |
toast.success('Successfully connected to Netlify'); | |
} catch (error) { | |
console.error('Auth error:', error); | |
logStore.logError('Failed to authenticate with Netlify', { error }); | |
toast.error('Failed to connect to Netlify'); | |
updateNetlifyConnection({ user: null, token: '' }); | |
} finally { | |
isConnecting.set(false); | |
} | |
}; | |
const handleDisconnect = () => { | |
updateNetlifyConnection({ user: null, token: '' }); | |
toast.success('Disconnected from Netlify'); | |
}; | |
return ( | |
<motion.div | |
className="bg-[#FFFFFF] dark:bg-[#0A0A0A] rounded-lg border border-[#E5E5E5] dark:border-[#1A1A1A]" | |
initial={{ opacity: 0, y: 20 }} | |
animate={{ opacity: 1, y: 0 }} | |
transition={{ delay: 0.3 }} | |
> | |
<div className="p-6 space-y-6"> | |
<div className="flex items-center justify-between"> | |
<div className="flex items-center gap-2"> | |
<img | |
className="w-5 h-5" | |
height="24" | |
width="24" | |
crossOrigin="anonymous" | |
src="https://cdn.simpleicons.org/netlify" | |
/> | |
<h3 className="text-base font-medium text-bolt-elements-textPrimary">Netlify Connection</h3> | |
</div> | |
</div> | |
{!connection.user ? ( | |
<div className="space-y-4"> | |
<div> | |
<label className="block text-sm text-bolt-elements-textSecondary mb-2">Personal Access Token</label> | |
<input | |
type="password" | |
value={connection.token} | |
onChange={(e) => updateNetlifyConnection({ ...connection, token: e.target.value })} | |
disabled={connecting} | |
placeholder="Enter your Netlify 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-[#00AD9F]', | |
'disabled:opacity-50', | |
)} | |
/> | |
<div className="mt-2 text-sm text-bolt-elements-textSecondary"> | |
<a | |
href="https://app.netlify.com/user/applications#personal-access-tokens" | |
target="_blank" | |
rel="noopener noreferrer" | |
className="text-[#00AD9F] hover:underline inline-flex items-center gap-1" | |
> | |
Get your token | |
<div className="i-ph:arrow-square-out w-4 h-4" /> | |
</a> | |
</div> | |
</div> | |
<button | |
onClick={handleConnect} | |
disabled={connecting || !connection.token} | |
className={classNames( | |
'px-4 py-2 rounded-lg text-sm flex items-center gap-2', | |
'bg-[#00AD9F] text-white', | |
'hover:bg-[#00968A]', | |
'disabled:opacity-50 disabled:cursor-not-allowed', | |
)} | |
> | |
{connecting ? ( | |
<> | |
<div className="i-ph:spinner-gap animate-spin" /> | |
Connecting... | |
</> | |
) : ( | |
<> | |
<div className="i-ph:plug-charging w-4 h-4" /> | |
Connect | |
</> | |
)} | |
</button> | |
</div> | |
) : ( | |
<div className="space-y-6"> | |
<div className="flex items-center justify-between"> | |
<div className="flex items-center gap-3"> | |
<button | |
onClick={handleDisconnect} | |
className={classNames( | |
'px-4 py-2 rounded-lg text-sm flex items-center gap-2', | |
'bg-red-500 text-white', | |
'hover:bg-red-600', | |
)} | |
> | |
<div className="i-ph:plug w-4 h-4" /> | |
Disconnect | |
</button> | |
<span className="text-sm text-bolt-elements-textSecondary flex items-center gap-1"> | |
<div className="i-ph:check-circle w-4 h-4 text-green-500" /> | |
Connected to Netlify | |
</span> | |
</div> | |
</div> | |
<div className="flex items-center gap-4 p-4 bg-[#F8F8F8] dark:bg-[#1A1A1A] rounded-lg"> | |
<img | |
src={connection.user.avatar_url} | |
referrerPolicy="no-referrer" | |
crossOrigin="anonymous" | |
alt={connection.user.full_name} | |
className="w-12 h-12 rounded-full border-2 border-[#00AD9F]" | |
/> | |
<div> | |
<h4 className="text-sm font-medium text-bolt-elements-textPrimary">{connection.user.full_name}</h4> | |
<p className="text-sm text-bolt-elements-textSecondary">{connection.user.email}</p> | |
</div> | |
</div> | |
{fetchingStats ? ( | |
<div className="flex items-center gap-2 text-sm text-bolt-elements-textSecondary"> | |
<div className="i-ph:spinner-gap w-4 h-4 animate-spin" /> | |
Fetching Netlify sites... | |
</div> | |
) : ( | |
<div> | |
<button | |
onClick={() => setIsSitesExpanded(!isSitesExpanded)} | |
className="w-full bg-transparent text-left text-sm font-medium text-bolt-elements-textPrimary mb-3 flex items-center gap-2" | |
> | |
<div className="i-ph:buildings w-4 h-4" /> | |
Your Sites ({connection.stats?.totalSites || 0}) | |
<div | |
className={classNames( | |
'i-ph:caret-down w-4 h-4 ml-auto transition-transform', | |
isSitesExpanded ? 'rotate-180' : '', | |
)} | |
/> | |
</button> | |
{isSitesExpanded && connection.stats?.sites?.length ? ( | |
<div className="grid gap-3"> | |
{connection.stats.sites.map((site) => ( | |
<a | |
key={site.id} | |
href={site.admin_url} | |
target="_blank" | |
rel="noopener noreferrer" | |
className="block p-4 rounded-lg border border-[#E5E5E5] dark:border-[#1A1A1A] hover:border-[#00AD9F] dark:hover:border-[#00AD9F] transition-colors" | |
> | |
<div className="flex items-center justify-between"> | |
<div> | |
<h5 className="text-sm font-medium text-bolt-elements-textPrimary flex items-center gap-2"> | |
<div className="i-ph:globe w-4 h-4 text-[#00AD9F]" /> | |
{site.name} | |
</h5> | |
<div className="flex items-center gap-2 mt-2 text-xs text-bolt-elements-textSecondary"> | |
<a | |
href={site.url} | |
target="_blank" | |
rel="noopener noreferrer" | |
className="hover:text-[#00AD9F]" | |
> | |
{site.url} | |
</a> | |
{site.published_deploy && ( | |
<> | |
<span>•</span> | |
<span className="flex items-center gap-1"> | |
<div className="i-ph:clock w-3 h-3" /> | |
{new Date(site.published_deploy.published_at).toLocaleDateString()} | |
</span> | |
</> | |
)} | |
</div> | |
</div> | |
{site.build_settings?.provider && ( | |
<div className="text-xs text-bolt-elements-textSecondary px-2 py-1 rounded-md bg-[#F0F0F0] dark:bg-[#252525]"> | |
<span className="flex items-center gap-1"> | |
<div className="i-ph:git-branch w-3 h-3" /> | |
{site.build_settings.provider} | |
</span> | |
</div> | |
)} | |
</div> | |
</a> | |
))} | |
</div> | |
) : isSitesExpanded ? ( | |
<div className="text-sm text-bolt-elements-textSecondary flex items-center gap-2"> | |
<div className="i-ph:info w-4 h-4" /> | |
No sites found in your Netlify account | |
</div> | |
) : null} | |
</div> | |
)} | |
</div> | |
)} | |
</div> | |
</motion.div> | |
); | |
} | |