add: connection improvements

Improve connections visually and functionality
This commit is contained in:
KevIsDev
2025-02-25 00:39:39 +00:00
parent 4da13d1edc
commit 96a0b2a066
5 changed files with 312 additions and 329 deletions

View File

@@ -67,124 +67,6 @@ interface GitHubConnection {
}
export default function ConnectionsTab() {
const [connection, setConnection] = useState<GitHubConnection>({
user: null,
token: '',
tokenType: 'classic',
});
const [isLoading, setIsLoading] = useState(true);
// Load saved connection on mount
useEffect(() => {
const savedConnection = localStorage.getItem('github_connection');
if (savedConnection) {
const parsed = JSON.parse(savedConnection);
// Ensure backward compatibility with existing connections
if (!parsed.tokenType) {
parsed.tokenType = 'classic';
}
setConnection(parsed);
if (parsed.user && parsed.token) {
fetchGitHubStats(parsed.token);
}
}
setIsLoading(false);
}, []);
const fetchGitHubStats = async (token: string) => {
try {
// Fetch repositories - only owned by the authenticated user
const reposResponse = await fetch(
'https://api.github.com/user/repos?sort=updated&per_page=10&affiliation=owner,organization_member,collaborator',
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
if (!reposResponse.ok) {
throw new Error('Failed to fetch repositories');
}
const repos = (await reposResponse.json()) as GitHubRepoInfo[];
// Fetch organizations
const orgsResponse = await fetch('https://api.github.com/user/orgs', {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!orgsResponse.ok) {
throw new Error('Failed to fetch organizations');
}
const organizations = (await orgsResponse.json()) as GitHubOrganization[];
// Fetch recent activity
const eventsResponse = await fetch('https://api.github.com/users/' + connection.user?.login + '/events/public', {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!eventsResponse.ok) {
throw new Error('Failed to fetch events');
}
const recentActivity = ((await eventsResponse.json()) as GitHubEvent[]).slice(0, 5);
// Fetch languages for each repository
const languagePromises = repos.map((repo) =>
fetch(repo.languages_url, {
headers: {
Authorization: `Bearer ${token}`,
},
}).then((res) => res.json() as Promise<Record<string, number>>),
);
const repoLanguages = await Promise.all(languagePromises);
const languages: GitHubLanguageStats = {};
repoLanguages.forEach((repoLang) => {
Object.entries(repoLang).forEach(([lang, bytes]) => {
languages[lang] = (languages[lang] || 0) + bytes;
});
});
// Calculate total stats
const totalStars = repos.reduce((acc, repo) => acc + repo.stargazers_count, 0);
const totalForks = repos.reduce((acc, repo) => acc + repo.forks_count, 0);
const totalGists = connection.user?.public_gists || 0;
setConnection((prev) => ({
...prev,
stats: {
repos,
totalStars,
totalForks,
organizations,
recentActivity,
languages,
totalGists,
},
}));
} catch (error) {
logStore.logError('Failed to fetch GitHub stats', { error });
toast.error('Failed to fetch GitHub statistics');
} finally {
}
};
if (isLoading) {
return <LoadingSpinner />;
}
return (
<div className="space-y-4">
@@ -211,14 +93,3 @@ export default function ConnectionsTab() {
</div>
);
}
function LoadingSpinner() {
return (
<div className="flex items-center justify-center p-4">
<div className="flex items-center gap-2">
<div className="i-ph:spinner-gap-bold animate-spin w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Loading...</span>
</div>
</div>
);
}

View File

@@ -71,26 +71,22 @@ export function GithubConnection() {
token: '',
tokenType: 'classic',
});
const [isLoading, setIsLoading] = useState(true);
const [isConnecting, setIsConnecting] = useState(false);
const [isFetchingStats, setIsFetchingStats] = useState(false);
const [expandedSections, setExpandedSections] = useState({
organizations: false,
languages: false,
recentActivity: false,
repositories: false,
});
useEffect(() => {
const savedConnection = localStorage.getItem('github_connection');
if (savedConnection) {
const parsed = JSON.parse(savedConnection);
if (!parsed.tokenType) {
parsed.tokenType = 'classic';
}
setConnection(parsed);
if (parsed.user && parsed.token) {
fetchGitHubStats(parsed.token);
}
}
}, []);
const toggleSection = (section: keyof typeof expandedSections) => {
setExpandedSections(prev => ({
...prev,
[section]: !prev[section]
}));
};
const fetchGitHubStats = async (token: string) => {
try {
@@ -176,6 +172,29 @@ export function GithubConnection() {
}
};
useEffect(() => {
const savedConnection = localStorage.getItem('github_connection');
if (savedConnection) {
const parsed = JSON.parse(savedConnection);
if (!parsed.tokenType) {
parsed.tokenType = 'classic';
}
setConnection(parsed);
if (parsed.user && parsed.token) {
fetchGitHubStats(parsed.token);
}
}
setIsLoading(false);
}, []);
if (isLoading) {
return <LoadingSpinner />;
}
const fetchGithubUser = async (token: string) => {
try {
setIsConnecting(true);
@@ -334,7 +353,7 @@ export function GithubConnection() {
'hover:bg-red-600',
)}
>
<div className="i-ph:plug-x w-4 h-4" />
<div className="i-ph:plug w-4 h-4" />
Disconnect
</button>
)}
@@ -347,42 +366,6 @@ export function GithubConnection() {
)}
</div>
{connection.user && (
<div className="p-4 bg-[#F8F8F8] dark:bg-[#1A1A1A] rounded-lg">
<div className="flex items-center gap-4">
<img src={connection.user.avatar_url} alt={connection.user.login} className="w-12 h-12 rounded-full" />
<div>
<h4 className="text-sm font-medium text-bolt-elements-textPrimary">{connection.user.name}</h4>
<p className="text-sm text-bolt-elements-textSecondary">@{connection.user.login}</p>
</div>
</div>
{isFetchingStats ? (
<div className="mt-4 flex items-center gap-2 text-sm text-bolt-elements-textSecondary">
<div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
Fetching GitHub stats...
</div>
) : (
connection.stats && (
<div className="mt-4 grid grid-cols-3 gap-4">
<div>
<p className="text-sm text-bolt-elements-textSecondary">Public Repos</p>
<p className="text-lg font-medium text-bolt-elements-textPrimary">{connection.user.public_repos}</p>
</div>
<div>
<p className="text-sm text-bolt-elements-textSecondary">Total Stars</p>
<p className="text-lg font-medium text-bolt-elements-textPrimary">{connection.stats.totalStars}</p>
</div>
<div>
<p className="text-sm text-bolt-elements-textSecondary">Total Forks</p>
<p className="text-lg font-medium text-bolt-elements-textPrimary">{connection.stats.totalForks}</p>
</div>
</div>
)
)}
</div>
)}
{connection.user && connection.stats && (
<div className="mt-6 border-t border-[#E5E5E5] dark:border-[#1A1A1A] pt-6">
<div className="flex items-center gap-4 mb-6">
@@ -399,6 +382,10 @@ export function GithubConnection() {
<div className="i-ph:users w-4 h-4" />
{connection.user.followers} followers
</span>
<span className="flex items-center gap-1">
<div className="i-ph:book-bookmark w-4 h-4" />
{connection.user.public_repos} public repos
</span>
<span className="flex items-center gap-1">
<div className="i-ph:star w-4 h-4" />
{connection.stats.totalStars} stars
@@ -413,139 +400,163 @@ export function GithubConnection() {
{/* Organizations Section */}
{connection.stats.organizations.length > 0 && (
<div className="mb-6">
<h4 className="text-sm font-medium text-bolt-elements-textPrimary mb-3">Organizations</h4>
<div className="flex flex-wrap gap-3">
{connection.stats.organizations.map((org) => (
<a
key={org.login}
href={org.html_url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 p-2 rounded-lg bg-[#F8F8F8] dark:bg-[#1A1A1A] hover:bg-[#F0F0F0] dark:hover:bg-[#252525] transition-colors"
>
<img src={org.avatar_url} alt={org.login} className="w-6 h-6 rounded-md" />
<span className="text-sm text-bolt-elements-textPrimary">{org.login}</span>
</a>
))}
</div>
<div className="space-y-3">
<button
onClick={() => toggleSection('organizations')}
className="w-full bg-transparent text-left text-sm font-medium text-bolt-elements-textPrimary flex items-center gap-2"
>
<div className="i-ph:buildings w-4 h-4" />
Organizations ({connection.stats.organizations.length})
<div className={classNames(
"i-ph:caret-down w-4 h-4 ml-auto transition-transform",
expandedSections.organizations ? "rotate-180" : ""
)} />
</button>
{expandedSections.organizations && (
<div className="flex flex-wrap gap-3 pb-4">
{connection.stats.organizations.map((org) => (
<a
key={org.login}
href={org.html_url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 p-2 rounded-lg bg-[#F8F8F8] dark:bg-[#1A1A1A] hover:bg-[#F0F0F0] dark:hover:bg-[#252525] transition-colors"
>
<img src={org.avatar_url} alt={org.login} className="w-6 h-6 rounded-md" />
<span className="text-sm text-bolt-elements-textPrimary">{org.login}</span>
</a>
))}
</div>
)}
</div>
)}
{/* Languages Section */}
<div className="mb-6">
<h4 className="text-sm font-medium text-bolt-elements-textPrimary mb-3">Top Languages</h4>
<div className="flex flex-wrap gap-2">
{Object.entries(connection.stats.languages)
.sort(([, a], [, b]) => b - a)
.slice(0, 5)
.map(([language]) => (
<span
key={language}
className="px-3 py-1 text-xs rounded-full bg-purple-500/10 text-purple-500 dark:bg-purple-500/20"
>
{language}
</span>
))}
</div>
<div className="space-y-3">
<button
onClick={() => toggleSection('languages')}
className="w-full bg-transparent text-left text-sm font-medium text-bolt-elements-textPrimary flex items-center gap-2"
>
<div className="i-ph:code w-4 h-4" />
Top Languages ({Object.keys(connection.stats.languages).length})
<div className={classNames(
"i-ph:caret-down w-4 h-4 ml-auto transition-transform",
expandedSections.languages ? "rotate-180" : ""
)} />
</button>
{expandedSections.languages && (
<div className="flex flex-wrap gap-2 pb-4">
{Object.entries(connection.stats.languages)
.sort(([, a], [, b]) => b - a)
.slice(0, 5)
.map(([language]) => (
<span
key={language}
className="px-3 py-1 text-xs rounded-full bg-purple-500/10 text-purple-500 dark:bg-purple-500/20"
>
{language}
</span>
))}
</div>
)}
</div>
{/* Recent Activity Section */}
<div className="mb-6">
<h4 className="text-sm font-medium text-bolt-elements-textPrimary mb-3">Recent Activity</h4>
<div className="space-y-3">
{connection.stats.recentActivity.map((event) => (
<div key={event.id} className="p-3 rounded-lg bg-[#F8F8F8] dark:bg-[#1A1A1A] text-sm">
<div className="flex items-center gap-2 text-bolt-elements-textPrimary">
<div className="i-ph:git-commit w-4 h-4 text-bolt-elements-textSecondary" />
<span className="font-medium">{event.type.replace('Event', '')}</span>
<span>on</span>
<a
href={`https://github.com/${event.repo.name}`}
target="_blank"
rel="noopener noreferrer"
className="text-purple-500 hover:underline"
>
{event.repo.name}
</a>
<div className="space-y-3">
<button
onClick={() => toggleSection('recentActivity')}
className="w-full bg-transparent text-left text-sm font-medium text-bolt-elements-textPrimary flex items-center gap-2"
>
<div className="i-ph:activity w-4 h-4" />
Recent Activity ({connection.stats.recentActivity.length})
<div className={classNames(
"i-ph:caret-down w-4 h-4 ml-auto transition-transform",
expandedSections.recentActivity ? "rotate-180" : ""
)} />
</button>
{expandedSections.recentActivity && (
<div className="space-y-3 pb-4">
{connection.stats.recentActivity.map((event) => (
<div key={event.id} className="p-3 rounded-lg bg-[#F8F8F8] dark:bg-[#1A1A1A] text-sm">
<div className="flex items-center gap-2 text-bolt-elements-textPrimary">
<div className="i-ph:git-commit w-4 h-4 text-bolt-elements-textSecondary" />
<span className="font-medium">{event.type.replace('Event', '')}</span>
<span>on</span>
<a
href={`https://github.com/${event.repo.name}`}
target="_blank"
rel="noopener noreferrer"
className="text-purple-500 hover:underline"
>
{event.repo.name}
</a>
</div>
<div className="mt-1 text-xs text-bolt-elements-textSecondary">
{new Date(event.created_at).toLocaleDateString()} at{' '}
{new Date(event.created_at).toLocaleTimeString()}
</div>
</div>
<div className="mt-1 text-xs text-bolt-elements-textSecondary">
{new Date(event.created_at).toLocaleDateString()} at{' '}
{new Date(event.created_at).toLocaleTimeString()}
</div>
</div>
))}
</div>
</div>
{/* Additional Stats */}
<div className="grid grid-cols-4 gap-4 mb-6">
<div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#1A1A1A]">
<div className="text-sm text-bolt-elements-textSecondary">Member Since</div>
<div className="text-lg font-medium text-bolt-elements-textPrimary">
{new Date(connection.user.created_at).toLocaleDateString()}
))}
</div>
</div>
<div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#1A1A1A]">
<div className="text-sm text-bolt-elements-textSecondary">Public Gists</div>
<div className="text-lg font-medium text-bolt-elements-textPrimary">{connection.stats.totalGists}</div>
</div>
<div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#1A1A1A]">
<div className="text-sm text-bolt-elements-textSecondary">Organizations</div>
<div className="text-lg font-medium text-bolt-elements-textPrimary">
{connection.stats.organizations.length}
</div>
</div>
<div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#1A1A1A]">
<div className="text-sm text-bolt-elements-textSecondary">Languages</div>
<div className="text-lg font-medium text-bolt-elements-textPrimary">
{Object.keys(connection.stats.languages).length}
</div>
</div>
)}
</div>
{/* Repositories Section */}
<h4 className="text-sm font-medium text-bolt-elements-textPrimary mb-3">Recent Repositories</h4>
<div className="space-y-3">
{connection.stats.repos.map((repo) => (
<a
key={repo.full_name}
href={repo.html_url}
target="_blank"
rel="noopener noreferrer"
className="block p-3 rounded-lg bg-[#F8F8F8] dark:bg-[#1A1A1A] hover:bg-[#F0F0F0] dark:hover:bg-[#252525] 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:git-repository w-4 h-4 text-bolt-elements-textSecondary" />
{repo.name}
</h5>
{repo.description && (
<p className="text-xs text-bolt-elements-textSecondary mt-1">{repo.description}</p>
)}
<div className="flex items-center gap-2 mt-2 text-xs text-bolt-elements-textSecondary">
<span className="flex items-center gap-1">
<div className="i-ph:git-branch w-3 h-3" />
{repo.default_branch}
</span>
<span></span>
<span>Updated {new Date(repo.updated_at).toLocaleDateString()}</span>
<button
onClick={() => toggleSection('repositories')}
className="w-full bg-transparent text-left text-sm font-medium text-bolt-elements-textPrimary flex items-center gap-2"
>
<div className="i-ph:clock-counter-clockwise w-4 h-4" />
Recent Repositories ({connection.stats.repos.length})
<div className={classNames(
"i-ph:caret-down w-4 h-4 ml-auto transition-transform",
expandedSections.repositories ? "rotate-180" : ""
)} />
</button>
{expandedSections.repositories && (
<div className="space-y-3">
{connection.stats.repos.map((repo) => (
<a
key={repo.full_name}
href={repo.html_url}
target="_blank"
rel="noopener noreferrer"
className="block p-3 rounded-lg bg-[#F8F8F8] dark:bg-[#1A1A1A] hover:bg-[#F0F0F0] dark:hover:bg-[#252525] 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:git-repository w-4 h-4 text-bolt-elements-textSecondary" />
{repo.name}
</h5>
{repo.description && (
<p className="text-xs text-bolt-elements-textSecondary mt-1">{repo.description}</p>
)}
<div className="flex items-center gap-2 mt-2 text-xs text-bolt-elements-textSecondary">
<span className="flex items-center gap-1">
<div className="i-ph:git-branch w-3 h-3" />
{repo.default_branch}
</span>
<span></span>
<span>Updated {new Date(repo.updated_at).toLocaleDateString()}</span>
</div>
</div>
<div className="flex items-center gap-3 text-xs text-bolt-elements-textSecondary">
<span className="flex items-center gap-1">
<div className="i-ph:star w-3 h-3" />
{repo.stargazers_count}
</span>
<span className="flex items-center gap-1">
<div className="i-ph:git-fork w-3 h-3" />
{repo.forks_count}
</span>
</div>
</div>
</div>
<div className="flex items-center gap-3 text-xs text-bolt-elements-textSecondary">
<span className="flex items-center gap-1">
<div className="i-ph:star w-3 h-3" />
{repo.stargazers_count}
</span>
<span className="flex items-center gap-1">
<div className="i-ph:git-fork w-3 h-3" />
{repo.forks_count}
</span>
</div>
</div>
</a>
))}
</a>
))}
</div>
)}
</div>
</div>
)}
@@ -553,3 +564,14 @@ export function GithubConnection() {
</motion.div>
);
}
function LoadingSpinner() {
return (
<div className="flex items-center justify-center p-4">
<div className="flex items-center gap-2">
<div className="i-ph:spinner-gap-bold animate-spin w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Loading...</span>
</div>
</div>
);
}

View File

@@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { motion } from 'framer-motion';
import { toast } from 'react-toastify';
import { useStore } from '@nanostores/react';
@@ -11,8 +11,8 @@ export function NetlifyConnection() {
const connection = useStore(netlifyConnection);
const connecting = useStore(isConnecting);
const fetchingStats = useStore(isFetchingStats);
const [isSitesExpanded, setIsSitesExpanded] = useState(false);
// Update the useEffect to handle the fetching state properly
useEffect(() => {
const fetchSites = async () => {
if (connection.user && connection.token) {
@@ -175,21 +175,21 @@ export function NetlifyConnection() {
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<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>
<button
onClick={handleDisconnect}
className={classNames(
'px-3 py-1.5 rounded-lg text-sm flex items-center gap-2',
'text-white bg-red-800 border border-red-500',
'hover:bg-red-50 dark:hover:bg-red-950',
'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>
@@ -214,11 +214,18 @@ export function NetlifyConnection() {
</div>
) : (
<div>
<h4 className="text-sm font-medium text-bolt-elements-textPrimary mb-3 flex items-center gap-2">
<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})
</h4>
{connection.stats?.sites?.length ? (
<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
@@ -266,12 +273,12 @@ export function NetlifyConnection() {
</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>