From 96a0b2a06689f0ec6bbfad7d96a5061026c5d9b0 Mon Sep 17 00:00:00 2001 From: KevIsDev Date: Tue, 25 Feb 2025 00:39:39 +0000 Subject: [PATCH] add: connection improvements Improve connections visually and functionality --- .../tabs/connections/ConnectionsTab.tsx | 129 ------ .../tabs/connections/GithubConnection.tsx | 368 ++++++++++-------- .../tabs/connections/NetlifyConnection.tsx | 35 +- .../header/HeaderActionButtons.client.tsx | 108 ++++- app/lib/persistence/db.ts | 1 + 5 files changed, 312 insertions(+), 329 deletions(-) diff --git a/app/components/@settings/tabs/connections/ConnectionsTab.tsx b/app/components/@settings/tabs/connections/ConnectionsTab.tsx index 4e582eea..ac55f2cd 100644 --- a/app/components/@settings/tabs/connections/ConnectionsTab.tsx +++ b/app/components/@settings/tabs/connections/ConnectionsTab.tsx @@ -67,124 +67,6 @@ interface GitHubConnection { } export default function ConnectionsTab() { - const [connection, setConnection] = useState({ - 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>), - ); - - 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 ; - } return (
@@ -211,14 +93,3 @@ export default function ConnectionsTab() {
); } - -function LoadingSpinner() { - return ( -
-
-
- Loading... -
-
- ); -} diff --git a/app/components/@settings/tabs/connections/GithubConnection.tsx b/app/components/@settings/tabs/connections/GithubConnection.tsx index c9ee632b..771eabdb 100644 --- a/app/components/@settings/tabs/connections/GithubConnection.tsx +++ b/app/components/@settings/tabs/connections/GithubConnection.tsx @@ -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 ; + } + const fetchGithubUser = async (token: string) => { try { setIsConnecting(true); @@ -334,7 +353,7 @@ export function GithubConnection() { 'hover:bg-red-600', )} > -
+
Disconnect )} @@ -347,42 +366,6 @@ export function GithubConnection() { )}
- {connection.user && ( -
-
- {connection.user.login} -
-

{connection.user.name}

-

@{connection.user.login}

-
-
- - {isFetchingStats ? ( -
-
- Fetching GitHub stats... -
- ) : ( - connection.stats && ( -
-
-

Public Repos

-

{connection.user.public_repos}

-
-
-

Total Stars

-

{connection.stats.totalStars}

-
-
-

Total Forks

-

{connection.stats.totalForks}

-
-
- ) - )} -
- )} - {connection.user && connection.stats && (
@@ -399,6 +382,10 @@ export function GithubConnection() {
{connection.user.followers} followers + +
+ {connection.user.public_repos} public repos +
{connection.stats.totalStars} stars @@ -413,139 +400,163 @@ export function GithubConnection() { {/* Organizations Section */} {connection.stats.organizations.length > 0 && ( -
-

Organizations

-
- {connection.stats.organizations.map((org) => ( - - {org.login} - {org.login} - - ))} -
+
+ + {expandedSections.organizations && ( +
+ {connection.stats.organizations.map((org) => ( + + {org.login} + {org.login} + + ))} +
+ )}
)} {/* Languages Section */} -
-

Top Languages

-
- {Object.entries(connection.stats.languages) - .sort(([, a], [, b]) => b - a) - .slice(0, 5) - .map(([language]) => ( - - {language} - - ))} -
+
+ + {expandedSections.languages && ( +
+ {Object.entries(connection.stats.languages) + .sort(([, a], [, b]) => b - a) + .slice(0, 5) + .map(([language]) => ( + + {language} + + ))} +
+ )}
{/* Recent Activity Section */} -
-

Recent Activity

-
- {connection.stats.recentActivity.map((event) => ( -
-
-
- {event.type.replace('Event', '')} - on - - {event.repo.name} - +
+ + {expandedSections.recentActivity && ( +
+ {connection.stats.recentActivity.map((event) => ( +
+
+
+ {event.type.replace('Event', '')} + on + + {event.repo.name} + +
+
+ {new Date(event.created_at).toLocaleDateString()} at{' '} + {new Date(event.created_at).toLocaleTimeString()} +
-
- {new Date(event.created_at).toLocaleDateString()} at{' '} - {new Date(event.created_at).toLocaleTimeString()} -
-
- ))} -
-
- - {/* Additional Stats */} -
-
-
Member Since
-
- {new Date(connection.user.created_at).toLocaleDateString()} + ))}
-
-
-
Public Gists
-
{connection.stats.totalGists}
-
-
-
Organizations
-
- {connection.stats.organizations.length} -
-
-
-
Languages
-
- {Object.keys(connection.stats.languages).length} -
-
+ )}
{/* Repositories Section */} -

Recent Repositories

- {connection.stats.repos.map((repo) => ( - -
-
-
-
- {repo.name} -
- {repo.description && ( -

{repo.description}

- )} -
- -
- {repo.default_branch} - - - Updated {new Date(repo.updated_at).toLocaleDateString()} + + {expandedSections.repositories && ( +
+ {connection.stats.repos.map((repo) => ( + + )} @@ -553,3 +564,14 @@ export function GithubConnection() { ); } + +function LoadingSpinner() { + return ( +
+
+
+ Loading... +
+
+ ); +} \ No newline at end of file diff --git a/app/components/@settings/tabs/connections/NetlifyConnection.tsx b/app/components/@settings/tabs/connections/NetlifyConnection.tsx index bdd08285..920bf646 100644 --- a/app/components/@settings/tabs/connections/NetlifyConnection.tsx +++ b/app/components/@settings/tabs/connections/NetlifyConnection.tsx @@ -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() {
- -
- Connected to Netlify - + +
+ Connected to Netlify +
@@ -214,11 +214,18 @@ export function NetlifyConnection() {
) : (
-

+

- {connection.stats?.sites?.length ? ( +
+ + {isSitesExpanded && connection.stats?.sites?.length ? (
{connection.stats.sites.map((site) => ( ))}
- ) : ( + ) : isSitesExpanded ? (
No sites found in your Netlify account
- )} + ) : null}
)}
diff --git a/app/components/header/HeaderActionButtons.client.tsx b/app/components/header/HeaderActionButtons.client.tsx index 8539460a..d4ccb181 100644 --- a/app/components/header/HeaderActionButtons.client.tsx +++ b/app/components/header/HeaderActionButtons.client.tsx @@ -7,8 +7,9 @@ import { workbenchStore } from '~/lib/stores/workbench'; import { webcontainer } from '~/lib/webcontainer'; import { classNames } from '~/utils/classNames'; import { path } from '~/utils/path'; -import { useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import type { ActionCallbackData } from '~/lib/runtime/message-parser'; +import { chatId } from '~/lib/persistence/useChatHistory'; // Add this import interface HeaderActionButtonsProps {} @@ -22,6 +23,20 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) { const [isDeploying, setIsDeploying] = useState(false); const isSmallViewport = useViewport(1024); const canHideChat = showWorkbench || !showChat; + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const dropdownRef = useRef(null); + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsDropdownOpen(false); + } + } + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const currentChatId = useStore(chatId); const handleDeploy = async () => { if (!connection.user || !connection.token) { @@ -29,6 +44,11 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) { return; } + if (!currentChatId) { + toast.error('No active chat found'); + return; + } + try { setIsDeploying(true); @@ -89,7 +109,8 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) { } const fileContents = await getAllFiles(buildPath); - const existingSiteId = localStorage.getItem(`netlify-site-${artifact.id}`); + // Use chatId instead of artifact.id + const existingSiteId = localStorage.getItem(`netlify-site-${currentChatId}`); // Deploy using the API route with file contents const response = await fetch('/api/deploy', { @@ -101,7 +122,7 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) { siteId: existingSiteId || undefined, files: fileContents, token: connection.token, - chatId: artifact.id, + chatId: currentChatId, // Use chatId instead of artifact.id }), }); @@ -153,7 +174,7 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) { // Store the site ID if it's a new site if (data.site) { - localStorage.setItem(`netlify-site-${artifact.id}`, data.site.id); + localStorage.setItem(`netlify-site-${currentChatId}`, data.site.id); } toast.success( @@ -179,15 +200,76 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) { return (
-
- +
+
+ +
+ + {isDropdownOpen && ( +
+ + + +
+ )}