mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-03-10 22:21:44 +00:00
- improved organisation of connections (collapsibles) - improved deploy button - improved unique chat deployments
292 lines
11 KiB
TypeScript
292 lines
11 KiB
TypeScript
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 } from '~/lib/stores/netlify';
|
|
import type { NetlifyUser, NetlifySite } 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 fetchNetlifyStats = async (token: string) => {
|
|
try {
|
|
isFetchingStats.set(true);
|
|
|
|
const sitesResponse = await fetch('https://api.netlify.com/api/v1/sites', {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
if (!sitesResponse.ok) {
|
|
throw new Error(`Failed to fetch sites: ${sitesResponse.status}`);
|
|
}
|
|
|
|
const sites = (await sitesResponse.json()) as NetlifySite[];
|
|
|
|
const currentState = netlifyConnection.get();
|
|
updateNetlifyConnection({
|
|
...currentState,
|
|
stats: {
|
|
sites,
|
|
totalSites: sites.length,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error('Netlify API Error:', error);
|
|
logStore.logError('Failed to fetch Netlify stats', { error });
|
|
toast.error('Failed to fetch Netlify statistics');
|
|
} finally {
|
|
isFetchingStats.set(false);
|
|
}
|
|
};
|
|
|
|
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>
|
|
);
|
|
}
|