bookwiz.io / components / GitHubRepoSelector.tsx
GitHubRepoSelector.tsx
Raw
'use client'

import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { supabase } from '@/lib/supabase'
import { IoLogoGithub, IoSearchOutline, IoAddOutline, IoLockClosedOutline, IoGlobeOutline, IoCalendarOutline } from 'react-icons/io5'
import GitHubCreateRepo from './GitHubCreateRepo'

interface Repository {
  id: number
  name: string
  full_name: string
  private: boolean
  updated_at: string
  description?: string
}

interface GitHubRepoSelectorProps {
  bookId: string
  onConnect: () => void
}

export default function GitHubRepoSelector({ bookId, onConnect }: GitHubRepoSelectorProps) {
  const router = useRouter()
  const [repositories, setRepositories] = useState<Repository[]>([])
  const [filteredRepos, setFilteredRepos] = useState<Repository[]>([])
  const [searchTerm, setSearchTerm] = useState('')
  const [isConnecting, setIsConnecting] = useState(false)
  const [selectedRepo, setSelectedRepo] = useState<Repository | null>(null)
  const [accessToken, setAccessToken] = useState<string>('')
  const [githubUsername, setGithubUsername] = useState<string>('')
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)
  const [showCreateForm, setShowCreateForm] = useState(false)

  useEffect(() => {
    const loadRepositories = async () => {
      try {
        // First, check if we have OAuth data from URL parameters (after GitHub OAuth redirect)
        const urlParams = new URLSearchParams(window.location.search)
        const repoDataParam = urlParams.get('repositories')
        const accessTokenParam = urlParams.get('accessToken')
        const githubUsernameParam = urlParams.get('githubUsername')

        if (repoDataParam && accessTokenParam && githubUsernameParam) {
          // Use URL parameters (fresh OAuth data)
          const decodedRepos = JSON.parse(Buffer.from(repoDataParam, 'base64').toString())
          setRepositories(decodedRepos)
          setFilteredRepos(decodedRepos)
          setAccessToken(accessTokenParam)
          setGithubUsername(githubUsernameParam)
          
          // Clean up URL parameters
          const cleanUrl = new URL(window.location.href)
          cleanUrl.searchParams.delete('repositories')
          cleanUrl.searchParams.delete('accessToken')
          cleanUrl.searchParams.delete('githubUsername')
          window.history.replaceState({}, '', cleanUrl.toString())
        } else {
          // Try to fetch from existing GitHub integration
          const { data: { session } } = await supabase.auth.getSession()
          const headers: Record<string, string> = {}
          if (session?.access_token) {
            headers['Authorization'] = `Bearer ${session.access_token}`
          }
          
          const response = await fetch('/api/github/repos', { headers })
          
          if (response.ok) {
            const data = await response.json()
            setRepositories(data.repositories)
            setFilteredRepos(data.repositories)
            setAccessToken(data.accessToken)
            setGithubUsername(data.githubUsername)
          } else if (response.status === 404) {
            // No GitHub integration found - this is expected for first-time users
            setError(null)
          } else {
            // Other error
            const errorData = await response.json()
            setError(errorData.error || 'Failed to load repositories')
          }
        }
      } catch (err) {
        console.error('Failed to load repository data:', err)
        setError('Failed to load repositories')
      } finally {
        setLoading(false)
      }
    }

    loadRepositories()
  }, [])

  useEffect(() => {
    const filtered = repositories.filter(repo =>
      repo.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
      repo.description?.toLowerCase().includes(searchTerm.toLowerCase()) ||
      repo.full_name.toLowerCase().includes(searchTerm.toLowerCase())
    )
    setFilteredRepos(filtered)
  }, [searchTerm, repositories])

  const handleConnectRepository = async (repo: Repository) => {
    if (!accessToken) return
    
    setIsConnecting(true)
    setSelectedRepo(repo)

    try {
      // Get current user
      const { data: { user } } = await supabase.auth.getUser()
      if (!user) {
        throw new Error('Please log in first')
      }

      // Create GitHub integration
      const integration = {
        provider: 'github',
        repository_url: `https://github.com/${repo.full_name}`,
        repository_name: repo.name,
        repository_full_name: repo.full_name,
        access_token: accessToken,
        github_username: githubUsername,
        repository_id: repo.id,
        is_private: repo.private,
        user_id: user.id
      }

      const response = await fetch(`/api/books/${bookId}/github-integration`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ integration }),
      })

      if (!response.ok) {
        const errorData = await response.json()
        throw new Error(errorData.error || 'Failed to connect repository')
      }

      // Success! Call the onConnect callback
      onConnect()

    } catch (error) {
      console.error('Error connecting repository:', error)
      setError(error instanceof Error ? error.message : 'Failed to connect repository')
    } finally {
      setIsConnecting(false)
      setSelectedRepo(null)
    }
  }

  const handleShowCreateForm = () => {
    setShowCreateForm(true)
  }

  const handleBackToRepoList = () => {
    setShowCreateForm(false)
  }

  const handleRepoCreated = () => {
    setShowCreateForm(false)
    onConnect() // Refresh the integration status
  }

  const formatDate = (dateString: string) => {
    return new Date(dateString).toLocaleDateString('en-US', {
      month: 'short',
      day: 'numeric'
    })
  }

  if (loading) {
    return (
      <div className="p-4 text-center">
        <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-teal-400 mx-auto mb-2"></div>
        <p className="text-xs text-slate-400">Loading repositories...</p>
      </div>
    )
  }

  if (error) {
    return (
      <div className="p-4">
        <div className="bg-red-900/20 border border-red-700 rounded-lg p-3 text-red-300 text-sm">
          {error}
        </div>
      </div>
    )
  }

  if (repositories.length === 0) {
    return (
      <div className="p-4 text-center">
        <IoLogoGithub className="w-8 h-8 text-slate-400 mx-auto mb-2" />
        <p className="text-sm text-slate-400 mb-2">No repositories found</p>
        <button
          onClick={handleShowCreateForm}
          className="text-xs bg-teal-600 hover:bg-teal-700 text-white px-3 py-1.5 rounded-md transition-colors"
        >
          Create Repository
        </button>
      </div>
    )
  }

  // Show create form if requested
  if (showCreateForm && accessToken && githubUsername) {
    return (
      <GitHubCreateRepo
        bookId={bookId}
        accessToken={accessToken}
        githubUsername={githubUsername}
        onBack={handleBackToRepoList}
        onCreate={handleRepoCreated}
      />
    )
  }

  return (
    <div className="flex flex-col h-full">
      {/* Header with GitHub username */}
      <div className="p-3 border-b border-slate-700">
        <div className="flex items-center gap-2 mb-2">
          <IoLogoGithub className="w-4 h-4 text-green-400" />
          <span className="text-sm font-semibold text-slate-200">Select Repository</span>
        </div>
        <div className="text-xs bg-green-900/20 text-green-400 px-2 py-1 rounded-md inline-block">
          @{githubUsername}
        </div>
      </div>

      {/* Search */}
      <div className="p-3 border-b border-slate-700">
        <div className="relative">
          <IoSearchOutline className="absolute left-3 top-1/2 transform -translate-y-1/2 w-3 h-3 text-slate-500" />
          <input
            type="text"
            placeholder="Search repositories..."
            value={searchTerm}
            onChange={(e) => setSearchTerm(e.target.value)}
            className="w-full pl-8 pr-3 py-2 text-xs bg-slate-800 border border-slate-700 rounded-md focus:outline-none focus:ring-1 focus:ring-teal-500 text-slate-200 placeholder-slate-500"
          />
        </div>
      </div>

      {/* Create New Repository Option */}
      <div className="p-3 border-b border-slate-700">
        <button
          onClick={handleShowCreateForm}
          className="w-full p-2 border-2 border-dashed border-slate-700 rounded-md hover:border-slate-600 transition-colors text-left group"
        >
          <div className="flex items-center gap-2">
            <div className="p-1 bg-teal-500/10 rounded group-hover:bg-teal-500/20 transition-colors">
              <IoAddOutline className="w-3 h-3 text-teal-400" />
            </div>
            <div>
              <div className="text-xs font-medium text-slate-200">Create New Repository</div>
              <div className="text-xs text-slate-400">Start fresh on GitHub</div>
            </div>
          </div>
        </button>
      </div>

      {/* Repository List */}
      <div className="flex-1 overflow-y-auto">
        {filteredRepos.length === 0 ? (
          <div className="p-4 text-center">
            <p className="text-xs text-slate-400">
              {searchTerm ? 'No repositories match your search' : 'No repositories found'}
            </p>
          </div>
        ) : (
          <div className="space-y-1 p-2">
            {filteredRepos.map((repo) => (
              <div
                key={repo.id}
                className="p-2 rounded-md border border-slate-700 hover:border-slate-600 hover:bg-slate-800/30 transition-colors group"
              >
                <div className="flex items-start justify-between gap-2">
                  <div className="flex-1 min-w-0">
                    <div className="flex items-center gap-1.5 mb-1">
                      <div className="text-sm font-medium text-slate-200 truncate">
                        {repo.name}
                      </div>
                      {repo.private ? (
                        <IoLockClosedOutline className="w-3 h-3 text-yellow-400 flex-shrink-0" />
                      ) : (
                        <IoGlobeOutline className="w-3 h-3 text-green-400 flex-shrink-0" />
                      )}
                    </div>
                    
                    {repo.description && (
                      <p className="text-xs text-slate-400 mb-1 line-clamp-2">
                        {repo.description}
                      </p>
                    )}
                    
                    <div className="flex items-center gap-2 text-xs text-slate-500">
                      <IoCalendarOutline className="w-3 h-3" />
                      <span>{formatDate(repo.updated_at)}</span>
                    </div>
                  </div>
                  
                  <button
                    onClick={() => handleConnectRepository(repo)}
                    disabled={isConnecting}
                    className={`px-2 py-1 text-xs rounded-md font-medium transition-colors flex-shrink-0 ${
                      isConnecting && selectedRepo?.id === repo.id
                        ? 'bg-slate-700 text-slate-400 cursor-not-allowed'
                        : 'bg-teal-600 text-white hover:bg-teal-700'
                    }`}
                  >
                    {isConnecting && selectedRepo?.id === repo.id ? (
                      <div className="flex items-center gap-1">
                        <div className="animate-spin rounded-full h-3 w-3 border border-slate-400 border-t-transparent"></div>
                        <span>Connecting...</span>
                      </div>
                    ) : (
                      'Connect'
                    )}
                  </button>
                </div>
              </div>
            ))}
          </div>
        )}
      </div>
    </div>
  )
}