import * as Dialog from '@radix-ui/react-dialog'; import { useState, useEffect, useCallback } from 'react'; import { toast } from 'react-toastify'; import { motion } from 'framer-motion'; import { Octokit } from '@octokit/rest'; // Internal imports import { getLocalStorage } from '~/shared/lib/persistence'; import { classNames } from '~/shared/utils/classNames'; import type { GitHubUserResponse } from '~/shared/components/github/types/GitHub'; import { logStore } from '~/shared/stores/logs'; import { workbenchStore } from '~/workbench/stores/workbench'; import { extractRelativePath } from '~/shared/utils/diff'; import { formatSize } from '~/shared/utils/formatSize'; import type { FileMap, File } from '~/workbench/stores/files'; // UI Components import { Badge, EmptyState, StatusIndicator, SearchInput } from '~/shared/components/ui'; interface PushToGitHubDialogProps { isOpen: boolean; onClose: () => void; onPush: (repoName: string, username?: string, token?: string, isPrivate?: boolean) => Promise; } interface GitHubRepo { name: string; full_name: string; html_url: string; description: string; stargazers_count: number; forks_count: number; default_branch: string; updated_at: string; language: string; private: boolean; } export function PushToGitHubDialog({ isOpen, onClose, onPush }: PushToGitHubDialogProps) { const [repoName, setRepoName] = useState(''); const [isPrivate, setIsPrivate] = useState(false); const [isLoading, setIsLoading] = useState(false); const [user, setUser] = useState(null); const [recentRepos, setRecentRepos] = useState([]); const [filteredRepos, setFilteredRepos] = useState([]); const [repoSearchQuery, setRepoSearchQuery] = useState(''); const [isFetchingRepos, setIsFetchingRepos] = useState(false); const [showSuccessDialog, setShowSuccessDialog] = useState(false); const [createdRepoUrl, setCreatedRepoUrl] = useState(''); const [pushedFiles, setPushedFiles] = useState<{ path: string; size: number }[]>([]); // Load GitHub connection on mount useEffect(() => { if (isOpen) { const connection = getLocalStorage('github_connection'); if (connection?.user && connection?.token) { setUser(connection.user); // Only fetch if we have both user and token if (connection.token.trim()) { fetchRecentRepos(connection.token); } } } }, [isOpen]); /* * Filter repositories based on search query * const debouncedSetRepoSearchQuery = useDebouncedCallback((value: string) => setRepoSearchQuery(value), 300); */ useEffect(() => { if (recentRepos.length === 0) { setFilteredRepos([]); return; } if (!repoSearchQuery.trim()) { setFilteredRepos(recentRepos); return; } const query = repoSearchQuery.toLowerCase().trim(); const filtered = recentRepos.filter( (repo) => repo.name.toLowerCase().includes(query) || (repo.description && repo.description.toLowerCase().includes(query)) || (repo.language && repo.language.toLowerCase().includes(query)), ); setFilteredRepos(filtered); }, [recentRepos, repoSearchQuery]); const fetchRecentRepos = useCallback(async (token: string) => { if (!token) { logStore.logError('No GitHub token available'); toast.error('GitHub authentication required'); return; } try { setIsFetchingRepos(true); console.log('Fetching GitHub repositories with token:', token.substring(0, 5) + '...'); // Fetch ALL repos by paginating through all pages let allRepos: GitHubRepo[] = []; let page = 1; let hasMore = true; while (hasMore) { const requestUrl = `https://api.github.com/user/repos?sort=updated&per_page=100&page=${page}&affiliation=owner,organization_member`; const response = await fetch(requestUrl, { headers: { Accept: 'application/vnd.github.v3+json', Authorization: `Bearer ${token.trim()}`, }, }); if (!response.ok) { let errorData: { message?: string } = {}; try { errorData = await response.json(); console.error('Error response data:', errorData); } catch (e) { errorData = { message: 'Could not parse error response' }; console.error('Could not parse error response:', e); } if (response.status === 401) { toast.error('GitHub token expired. Please reconnect your account.'); // Clear invalid token const connection = getLocalStorage('github_connection'); if (connection) { localStorage.removeItem('github_connection'); setUser(null); } } else if (response.status === 403 && response.headers.get('x-ratelimit-remaining') === '0') { // Rate limit exceeded const resetTime = response.headers.get('x-ratelimit-reset'); const resetDate = resetTime ? new Date(parseInt(resetTime) * 1000).toLocaleTimeString() : 'soon'; toast.error(`GitHub API rate limit exceeded. Limit resets at ${resetDate}`); } else { logStore.logError('Failed to fetch GitHub repositories', { status: response.status, statusText: response.statusText, error: errorData, }); toast.error(`Failed to fetch repositories: ${errorData.message || response.statusText}`); } return; } try { const repos = (await response.json()) as GitHubRepo[]; allRepos = allRepos.concat(repos); if (repos.length < 100) { hasMore = false; } else { page += 1; } } catch (parseError) { console.error('Error parsing JSON response:', parseError); logStore.logError('Failed to parse GitHub repositories response', { parseError }); toast.error('Failed to parse repository data'); setRecentRepos([]); return; } } setRecentRepos(allRepos); } catch (error) { console.error('Exception while fetching GitHub repositories:', error); logStore.logError('Failed to fetch GitHub repositories', { error }); toast.error('Failed to fetch recent repositories'); } finally { setIsFetchingRepos(false); } }, []); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); const connection = getLocalStorage('github_connection'); if (!connection?.token || !connection?.user) { toast.error('Please connect your GitHub account in Settings > Connections first'); return; } if (!repoName.trim()) { toast.error('Repository name is required'); return; } setIsLoading(true); try { // Check if repository exists first const octokit = new Octokit({ auth: connection.token }); try { const { data: existingRepo } = await octokit.repos.get({ owner: connection.user.login, repo: repoName, }); // If we get here, the repo exists let confirmMessage = `Repository "${repoName}" already exists. Do you want to update it? This will add or modify files in the repository.`; // Add visibility change warning if needed if (existingRepo.private !== isPrivate) { const visibilityChange = isPrivate ? 'This will also change the repository from public to private.' : 'This will also change the repository from private to public.'; confirmMessage += `\n\n${visibilityChange}`; } const confirmOverwrite = window.confirm(confirmMessage); if (!confirmOverwrite) { setIsLoading(false); return; } } catch (error) { // 404 means repo doesn't exist, which is what we want for new repos if (error instanceof Error && 'status' in error && error.status !== 404) { throw error; } } const repoUrl = await onPush(repoName, connection.user.login, connection.token, isPrivate); setCreatedRepoUrl(repoUrl); // Get list of pushed files const files = workbenchStore.files.get(); const filesList = Object.entries(files as FileMap) .filter(([, dirent]) => dirent?.type === 'file' && !dirent.isBinary) .map(([path, dirent]) => ({ path: extractRelativePath(path), size: new TextEncoder().encode((dirent as File).content || '').length, })); setPushedFiles(filesList); setShowSuccessDialog(true); } catch (error) { console.error('Error pushing to GitHub:', error); toast.error('Failed to push to GitHub. Please check your repository name and try again.'); } finally { setIsLoading(false); } } const handleClose = () => { setRepoName(''); setIsPrivate(false); setShowSuccessDialog(false); setCreatedRepoUrl(''); onClose(); }; // Success Dialog if (showSuccessDialog) { return ( !open && handleClose()}>

Successfully pushed to GitHub

Your code is now available on GitHub

Repository URL

{createdRepoUrl} { navigator.clipboard.writeText(createdRepoUrl); toast.success('URL copied to clipboard'); }} className="p-2 text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary dark:text-bolt-elements-textSecondary-dark dark:hover:text-bolt-elements-textPrimary-dark bg-bolt-elements-background-depth-1 dark:bg-bolt-elements-background-depth-4 rounded-lg border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark" whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} >

Pushed Files ({pushedFiles.length})

{pushedFiles.map((file) => (
{file.path} {formatSize(file.size)}
))}
View Repository { navigator.clipboard.writeText(createdRepoUrl); toast.success('URL copied to clipboard'); }} className="px-4 py-2 rounded-lg bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-3 text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondary-dark hover:bg-bolt-elements-background-depth-3 dark:hover:bg-bolt-elements-background-depth-4 text-sm inline-flex items-center gap-2 border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark" whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} >
Copy URL Close
); } if (!user) { return ( !open && handleClose()}>

GitHub Connection Required

To push your code to GitHub, you need to connect your GitHub account in Settings {'>'} Connections first.

Close
Go to Settings
); } return ( !open && handleClose()}>
Push to GitHub

Push your code to a new or existing GitHub repository

{user.login}

{user.name || user.login}

@{user.login}

setRepoName(e.target.value)} placeholder="my-awesome-project" className="w-full pl-10 px-4 py-2 rounded-lg bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary-dark placeholder-bolt-elements-textTertiary dark:placeholder-bolt-elements-textTertiary-dark focus:outline-none focus:ring-2 focus:ring-purple-500" required />
{filteredRepos.length} of {recentRepos.length}
setRepoSearchQuery(e.target.value)} onClear={() => setRepoSearchQuery('')} className="bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark text-sm" />
{recentRepos.length === 0 && !isFetchingRepos ? ( ) : (
{filteredRepos.length === 0 && repoSearchQuery.trim() !== '' ? ( ) : ( filteredRepos.map((repo) => ( setRepoName(repo.name)} className="w-full p-3 text-left rounded-lg bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-3 hover:bg-bolt-elements-background-depth-3 dark:hover:bg-bolt-elements-background-depth-4 transition-colors group border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark hover:border-purple-500/30" whileHover={{ scale: 1.01 }} whileTap={{ scale: 0.99 }} >
{repo.name}
{repo.private && ( Private )}
{repo.description && (

{repo.description}

)}
{repo.language && ( {repo.language} )} {repo.stargazers_count.toLocaleString()} {repo.forks_count.toLocaleString()} {new Date(repo.updated_at).toLocaleDateString()}
)) )}
)}
{isFetchingRepos && (
)}
setIsPrivate(e.target.checked)} className="rounded border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark text-purple-500 focus:ring-purple-500 dark:bg-bolt-elements-background-depth-3" />

Private repositories are only visible to you and people you share them with

Cancel {isLoading ? ( <>
Pushing... ) : ( <>
Push to GitHub )}
); }