Fixed filtering and simplified octokit context

This commit is contained in:
NasSharaf 2025-09-04 12:05:20 -04:00
parent 3a2a6dca7e
commit cd3cd7667a
4 changed files with 187 additions and 165 deletions

View File

@ -2,12 +2,12 @@
import { PageWrapper } from '@/components/foundation' import { PageWrapper } from '@/components/foundation'
import CheckBalanceIframe from '@/components/iframe/check-balance-iframe/CheckBalanceIframe' import CheckBalanceIframe from '@/components/iframe/check-balance-iframe/CheckBalanceIframe'
import { FixedProjectCard } from '@/components/projects/project/ProjectCard/FixedProjectCard' import { FixedProjectCard } from '@/components/projects/project/ProjectCard/FixedProjectCard'
import { Button } from '@workspace/ui/components/button'
import { useEffect, useState } from 'react'
import { Shapes } from 'lucide-react'
import { useGQLClient } from '@/context' import { useGQLClient } from '@/context'
import { useUser } from '@clerk/nextjs' import { useUser } from '@clerk/nextjs'
import type { Project } from '@workspace/gql-client' import type { Project } from '@workspace/gql-client'
import { Button } from '@workspace/ui/components/button'
import { Shapes } from 'lucide-react'
import { useEffect, useState } from 'react'
interface ProjectData { interface ProjectData {
id: string id: string
@ -24,7 +24,7 @@ export default function ProjectsPage() {
const [projects, setProjects] = useState<ProjectData[]>([]) const [projects, setProjects] = useState<ProjectData[]>([])
const [isLoading, setIsLoading] = useState<boolean>(true) const [isLoading, setIsLoading] = useState<boolean>(true)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const client = useGQLClient() const client = useGQLClient()
const { user } = useUser() const { user } = useUser()
@ -95,7 +95,7 @@ export default function ProjectsPage() {
console.log( console.log(
`🔍 Project ${project.name}: repo owner = ${repoOwner}, current user = ${githubUsername}` `🔍 Project ${project.name}: repo owner = ${repoOwner}, current user = ${githubUsername}`
) )
return repoOwner === githubUsername return repoOwner.toLowerCase() === githubUsername.toLowerCase()
} }
return true // Include projects without repository info return true // Include projects without repository info
}) })
@ -130,7 +130,7 @@ export default function ProjectsPage() {
setIsLoading(false) setIsLoading(false)
} }
} }
return ( return (
<PageWrapper <PageWrapper
header={{ header={{
@ -157,7 +157,7 @@ export default function ProjectsPage() {
<p className="text-gray-400 text-center max-w-md mb-6"> <p className="text-gray-400 text-center max-w-md mb-6">
Failed to load your deployed projects. Please try again. Failed to load your deployed projects. Please try again.
</p> </p>
<Button <Button
className="bg-white text-black hover:bg-gray-200 flex items-center" className="bg-white text-black hover:bg-gray-200 flex items-center"
onClick={loadAllProjects} onClick={loadAllProjects}
> >
@ -174,13 +174,20 @@ export default function ProjectsPage() {
</div> </div>
<h2 className="text-xl font-semibold mb-2">Deploy your first app</h2> <h2 className="text-xl font-semibold mb-2">Deploy your first app</h2>
<p className="text-gray-400 text-center max-w-md mb-6"> <p className="text-gray-400 text-center max-w-md mb-6">
You don't have any deployed projects yet. Create your first project to get started. You don't have any deployed projects yet. Create your first project
to get started.
</p> </p>
<Button <Button
className="bg-white text-black hover:bg-gray-200 flex items-center" className="bg-white text-black hover:bg-gray-200 flex items-center"
onClick={handleCreateProject} onClick={handleCreateProject}
> >
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <svg
className="mr-2 h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<line x1="12" y1="5" x2="12" y2="19"></line> <line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line> <line x1="5" y1="12" x2="19" y2="12"></line>
</svg> </svg>
@ -193,9 +200,11 @@ export default function ProjectsPage() {
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{projects.map((project) => { {projects.map((project) => {
// Get the current deployment for status // Get the current deployment for status
const currentDeployment = project.deployments.find(d => d.isCurrent) const currentDeployment = project.deployments.find(
(d) => d.isCurrent
)
const latestDeployment = project.deployments[0] // Assuming sorted by date const latestDeployment = project.deployments[0] // Assuming sorted by date
// Determine status based on deployment // Determine status based on deployment
let status = 'pending' let status = 'pending'
if (currentDeployment || latestDeployment) { if (currentDeployment || latestDeployment) {
@ -217,30 +226,34 @@ export default function ProjectsPage() {
status = 'pending' status = 'pending'
} }
} }
// Format the project data to match what FixedProjectCard expects // Format the project data to match what FixedProjectCard expects
const formattedProject = { const formattedProject = {
id: project.id, id: project.id,
name: project.name, name: project.name,
full_name: project.repository ? project.repository.replace('https://github.com/', '') : project.name, full_name: project.repository
? project.repository.replace('https://github.com/', '')
: project.name,
repository: project.repository, repository: project.repository,
framework: project.framework, framework: project.framework,
description: project.description, description: project.description,
// Ensure deployments array is properly formatted // Ensure deployments array is properly formatted
deployments: project.deployments.map(deployment => ({ deployments: project.deployments.map((deployment) => ({
...deployment, ...deployment,
// Make sure the date is in a format the card can parse // Make sure the date is in a format the card can parse
createdAt: deployment.createdAt, createdAt: deployment.createdAt,
applicationDeploymentRecordData: { applicationDeploymentRecordData: {
url: deployment.applicationDeploymentRecordData?.url || `https://${project.name.toLowerCase()}.example.com` url:
deployment.applicationDeploymentRecordData?.url ||
`https://${project.name.toLowerCase()}.example.com`
} }
})) }))
} }
return ( return (
<FixedProjectCard <FixedProjectCard
project={formattedProject} project={formattedProject}
key={project.id} key={project.id}
status={status as any} status={status as any}
/> />
) )
@ -248,7 +261,7 @@ export default function ProjectsPage() {
</div> </div>
</div> </div>
)} )}
{/* Wrap in try/catch to prevent breaking if there are issues */} {/* Wrap in try/catch to prevent breaking if there are issues */}
{(() => { {(() => {
try { try {
@ -258,12 +271,12 @@ export default function ProjectsPage() {
isPollingEnabled={false} isPollingEnabled={false}
amount="1" amount="1"
/> />
); )
} catch (error) { } catch (error) {
console.error('Failed to render CheckBalanceIframe:', error); console.error('Failed to render CheckBalanceIframe:', error)
return null; return null
} }
})()} })()}
</PageWrapper> </PageWrapper>
) )
} }

View File

@ -73,7 +73,17 @@ export function ConnectStep() {
} = useAuthStatus() } = useAuthStatus()
// Repository data // Repository data
const { repoData: repositories, isLoading: isLoadingRepos } = useRepoData('') const { repoData: repositories, isLoading: isLoadingRepos, error: repoError } = useRepoData('')
// Debug repository data
useEffect(() => {
console.log('🔍 ConnectStep: Repository data changed:', {
repositories: repositories ? `Array with ${Array.isArray(repositories) ? repositories.length : 'not array'} items` : 'null',
isLoadingRepos,
repoError,
isFullyAuthenticated
})
}, [repositories, isLoadingRepos, repoError, isFullyAuthenticated])
// Handle hydration mismatch by waiting for mount // Handle hydration mismatch by waiting for mount
useEffect(() => { useEffect(() => {

View File

@ -1,11 +1,11 @@
'use client' 'use client'
import { useEffect, useState } from 'react' import { useOnboarding } from '@/components/onboarding/store'
import { useRouter, useParams } from 'next/navigation'
import { useTheme } from 'next-themes'
import { CheckCircle } from 'lucide-react'
import { Button } from '@workspace/ui/components/button' import { Button } from '@workspace/ui/components/button'
import { useOnboarding } from '@/components/onboarding/useOnboarding' import { CheckCircle } from 'lucide-react'
import { useTheme } from 'next-themes'
import { useParams, useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
export function SuccessStep() { export function SuccessStep() {
const router = useRouter() const router = useRouter()
@ -14,45 +14,49 @@ export function SuccessStep() {
const [mounted, setMounted] = useState(false) const [mounted, setMounted] = useState(false)
const providerParam = params?.provider ? String(params.provider) : 'github' const providerParam = params?.provider ? String(params.provider) : 'github'
const { formData, resetOnboarding } = useOnboarding() const { formData, resetOnboarding } = useOnboarding()
// Handle hydration mismatch by waiting for mount // Handle hydration mismatch by waiting for mount
useEffect(() => { useEffect(() => {
setMounted(true) setMounted(true)
}, []) }, [])
// Get deployment info from form data // Get deployment info from form data - using available properties
const repoName = formData.githubRepo ? formData.githubRepo.split('/').pop() : (formData.projectName || 'project') const repoName = formData.githubRepo
const deploymentUrl = formData.deploymentUrl || `https://${repoName}.laconic.deploy` ? formData.githubRepo.split('/').pop()
const projectId = formData.projectId || 'unknown-id' : formData.repoName
? formData.repoName.split('/').pop()
: formData.projectName || 'project'
const deploymentUrl = `https://${repoName}.laconic.deploy` // Default deployment URL
const projectId = formData.projectId || formData.deploymentId || 'unknown-id' // Use projectId first, fallback to deploymentId
// Handle "View Project" button - navigates to project page // Handle "View Project" button - navigates to project page
const handleViewProject = () => { const handleViewProject = () => {
console.log('Navigating to project with ID:', projectId) console.log('Navigating to project with ID:', projectId)
resetOnboarding() // Reset state for next time resetOnboarding() // Reset state for next time
// Navigate to the project detail page using the GraphQL project ID // Navigate to the project detail page using the GraphQL project ID
router.push(`/projects/${providerParam}/ps/${projectId}`) router.push(`/projects/${providerParam}/ps/${projectId}`)
} }
// Auto-redirect after a delay (optional) // Auto-redirect after a delay (optional)
useEffect(() => { useEffect(() => {
if (mounted && projectId && projectId !== 'unknown-id') { if (mounted && projectId && projectId !== 'unknown-id') {
const timer = setTimeout(() => { const timer = setTimeout(() => {
handleViewProject() handleViewProject()
}, 3000) // Auto-redirect after 3 seconds }, 3000) // Auto-redirect after 3 seconds
return () => clearTimeout(timer) return () => clearTimeout(timer)
} }
}, [mounted, projectId]) }, [mounted, projectId])
// Don't render UI until after mount to prevent hydration mismatch // Don't render UI until after mount to prevent hydration mismatch
if (!mounted) { if (!mounted) {
return null return null
} }
// Determine if dark mode is active // Determine if dark mode is active
const isDarkMode = resolvedTheme === 'dark' const isDarkMode = resolvedTheme === 'dark'
return ( return (
<div className="w-full h-full flex flex-col items-center justify-center p-8"> <div className="w-full h-full flex flex-col items-center justify-center p-8">
<div className="max-w-md w-full mx-auto"> <div className="max-w-md w-full mx-auto">
@ -60,17 +64,21 @@ export function SuccessStep() {
<div className="mx-auto mb-6 flex justify-center"> <div className="mx-auto mb-6 flex justify-center">
<CheckCircle className="h-16 w-16 text-green-500" /> <CheckCircle className="h-16 w-16 text-green-500" />
</div> </div>
{/* Success header */} {/* Success header */}
<h2 className={`text-2xl font-medium ${isDarkMode ? "text-white" : "text-zinc-900"} text-center mb-2`}> <h2
className={`text-2xl font-medium ${isDarkMode ? 'text-white' : 'text-zinc-900'} text-center mb-2`}
>
Successfully Deployed! Successfully Deployed!
</h2> </h2>
<p className="text-center text-zinc-500 mb-8"> <p className="text-center text-zinc-500 mb-8">
Your project has been deployed successfully Your project has been deployed successfully
</p> </p>
{/* Deployment summary */} {/* Deployment summary */}
<div className={`border rounded-md p-4 mb-6 ${isDarkMode ? "border-zinc-700" : "border-zinc-300"}`}> <div
className={`border rounded-md p-4 mb-6 ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}
>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-muted-foreground">Project:</span> <span className="text-muted-foreground">Project:</span>
@ -86,39 +94,82 @@ export function SuccessStep() {
</div> </div>
</div> </div>
</div> </div>
{/* Next steps section */} {/* Next steps section */}
<div className="mb-8"> <div className="mb-8">
<h3 className={`text-lg font-medium ${isDarkMode ? "text-white" : "text-zinc-900"} mb-4`}>Next steps</h3> <h3
className={`text-lg font-medium ${isDarkMode ? 'text-white' : 'text-zinc-900'} mb-4`}
<div className={`border rounded-md overflow-hidden mb-4 ${isDarkMode ? "border-zinc-700" : "border-zinc-300"}`}> >
Next steps
</h3>
<div
className={`border rounded-md overflow-hidden mb-4 ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}
>
<div className="flex items-center p-4 justify-between"> <div className="flex items-center p-4 justify-between">
<div> <div>
<div className={isDarkMode ? "text-white font-medium" : "text-zinc-900 font-medium"}>Setup Domain</div> <div
<div className="text-zinc-500 text-sm">Add a custom domain to your project.</div> className={
isDarkMode
? 'text-white font-medium'
: 'text-zinc-900 font-medium'
}
>
Setup Domain
</div>
<div className="text-zinc-500 text-sm">
Add a custom domain to your project.
</div>
</div> </div>
<Button variant="outline" className={`rounded-full p-1 w-8 h-8 flex items-center justify-center ${isDarkMode ? "border-zinc-700" : "border-zinc-300"}`}> <Button
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={isDarkMode ? "text-white" : "text-zinc-900"}> variant="outline"
<path d="M9 18L15 12L9 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/> className={`rounded-full p-1 w-8 h-8 flex items-center justify-center ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={isDarkMode ? 'text-white' : 'text-zinc-900'}
>
<path
d="M9 18L15 12L9 6"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg> </svg>
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>
{/* Action buttons */} {/* Action buttons */}
<div className="flex flex-col space-y-3"> <div className="flex flex-col space-y-3">
<Button <Button
className="w-full bg-white hover:bg-white/90 text-black flex items-center justify-center" className="w-full bg-white hover:bg-white/90 text-black flex items-center justify-center"
onClick={handleViewProject} onClick={handleViewProject}
disabled={!projectId || projectId === 'unknown-id'} disabled={!projectId || projectId === 'unknown-id'}
> >
View Project View Project
<svg className="ml-2 h-4 w-4" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg
<path d="M5 12H19M19 12L13 6M19 12L13 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/> className="ml-2 h-4 w-4"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 12H19M19 12L13 6M19 12L13 18"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg> </svg>
</Button> </Button>
{/* Manual navigation button if auto-redirect fails */} {/* Manual navigation button if auto-redirect fails */}
{projectId === 'unknown-id' && ( {projectId === 'unknown-id' && (
<p className="text-center text-xs text-muted-foreground"> <p className="text-center text-xs text-muted-foreground">
@ -129,4 +180,4 @@ export function SuccessStep() {
</div> </div>
</div> </div>
) )
} }

View File

@ -1,8 +1,6 @@
'use client' 'use client'
import { getGitHubToken } from '@/actions/github' import { useOctokit } from '@/context/OctokitContext'
import { useAuth, useUser } from '@clerk/nextjs'
import { Octokit } from '@octokit/rest'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
// Define the return type of the hook // Define the return type of the hook
@ -22,113 +20,53 @@ export function useRepoData(repoId: string): UseRepoDataReturn {
const [repoData, setRepoData] = useState<any>(null) const [repoData, setRepoData] = useState<any>(null)
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [octokit, setOctokit] = useState<Octokit | null>(null)
// Get auth data from Clerk // Use the centralized Octokit context instead of creating our own
const { isLoaded: isAuthLoaded, getToken, isSignedIn } = useAuth() const { octokit, isAuth } = useOctokit()
const { isLoaded: isUserLoaded, user } = useUser()
// Initialize Octokit with the appropriate token // Debug the context state
useEffect(() => { useEffect(() => {
async function initializeOctokit() { console.log('🔍 useRepoData: Context state changed:', {
let token = null hasOctokit: !!octokit,
isAuth,
repoId
})
}, [octokit, isAuth, repoId])
// Try to get GitHub OAuth token from Clerk using multiple methods // Fetch repo data when Octokit is available and authenticated
if (isSignedIn && user) {
try {
// Check if user has connected GitHub account
const githubAccount = user.externalAccounts.find(
(account) => account.provider === 'github'
)
if (githubAccount) {
// Try multiple methods to get the GitHub OAuth token (same as test-connection page)
// Method 1: Try getToken from useAuth hook
try {
token = await getToken()
console.log('Method 1 (getToken from useAuth) worked:', token ? 'SUCCESS' : 'NO TOKEN')
} catch (error) {
console.log('Method 1 failed:', error)
}
// Method 2: Try with GitHub template parameter
if (!token) {
try {
token = await getToken({ template: 'github' })
console.log('Method 2 (getToken with github template) worked:', token ? 'SUCCESS' : 'NO TOKEN')
} catch (error) {
console.log('Method 2 failed:', error)
}
}
// Method 3: Try accessing window.Clerk
if (!token && typeof window !== 'undefined' && (window as any).Clerk) {
try {
token = await (window as any).Clerk.session?.getToken()
console.log('Method 3 (window.Clerk.session.getToken) worked:', token ? 'SUCCESS' : 'NO TOKEN')
} catch (error) {
console.log('Method 3 failed:', error)
}
}
// Method 4: Try window.Clerk with GitHub template
if (!token && typeof window !== 'undefined' && (window as any).Clerk) {
try {
token = await (window as any).Clerk.session?.getToken({ template: 'github' })
console.log('Method 4 (window.Clerk with github template) worked:', token ? 'SUCCESS' : 'NO TOKEN')
} catch (error) {
console.log('Method 4 failed:', error)
}
}
if (token) {
console.log('Successfully obtained GitHub token from Clerk')
} else {
console.warn('All methods failed to get GitHub OAuth token from Clerk')
}
}
} catch (err) {
console.error('Error accessing Clerk user data:', err)
}
}
// Fallback to token from environment variable
if (!token) {
token = process.env.NEXT_PUBLIC_GITHUB_FALLBACK_TOKEN || ''
if (token) {
console.warn(
'Using fallback GitHub token. This should only be used for development.'
)
}
}
// Create Octokit instance with whatever token we found
if (token) {
setOctokit(new Octokit({ auth: token }))
} else {
setError('No GitHub token available')
setIsLoading(false)
}
}
if (isAuthLoaded && isUserLoaded) {
initializeOctokit()
}
}, [isAuthLoaded, isUserLoaded, user])
// Fetch repo data when Octokit is available
useEffect(() => { useEffect(() => {
let isMounted = true let isMounted = true
async function fetchRepoData() { async function fetchRepoData() {
if (!octokit) { // Don't attempt to fetch if not authenticated
if (!isAuth) {
console.log('🔍 fetchRepoData: Not authenticated, skipping fetch')
if (isMounted) {
setError('GitHub authentication required')
setRepoData(null)
setIsLoading(false)
}
return return
} }
if (!octokit) {
console.log('🔍 fetchRepoData: No octokit instance available')
if (isMounted) {
setError('GitHub client not available')
setRepoData(null)
setIsLoading(false)
}
return
}
console.log('🔍 fetchRepoData: Starting to fetch repos...')
console.log('🔍 fetchRepoData: isAuth =', isAuth)
try { try {
// Fetch repos from GitHub // Fetch repos from GitHub
const { data: repos } = await octokit.repos.listForAuthenticatedUser() console.log('🔍 Making GitHub API call: octokit.rest.repos.listForAuthenticatedUser()')
const { data: repos } = await octokit.rest.repos.listForAuthenticatedUser()
console.log('🔍 GitHub API success! Received', repos.length, 'repositories')
// If no repoId is provided, return all repos // If no repoId is provided, return all repos
if (!repoId) { if (!repoId) {
@ -141,7 +79,7 @@ export function useRepoData(repoId: string): UseRepoDataReturn {
} }
// Find the specific repo by ID if repoId is provided // Find the specific repo by ID if repoId is provided
const repo = repos.find((repo) => repo.id.toString() === repoId) const repo = repos.find((repo: any) => repo.id.toString() === repoId)
if (!repo) { if (!repo) {
if (isMounted) { if (isMounted) {
@ -157,7 +95,12 @@ export function useRepoData(repoId: string): UseRepoDataReturn {
} }
} }
} catch (err) { } catch (err) {
console.error('Error fetching GitHub repo:', err) console.error('❌ Error fetching GitHub repo:', err)
console.error('❌ Error details:', {
message: err instanceof Error ? err.message : 'Unknown error',
status: (err as any)?.status,
response: (err as any)?.response?.data,
})
if (isMounted) { if (isMounted) {
setError('Failed to fetch repository data') setError('Failed to fetch repository data')
setRepoData(null) setRepoData(null)
@ -166,14 +109,19 @@ export function useRepoData(repoId: string): UseRepoDataReturn {
} }
} }
if (octokit) { if (octokit && isAuth) {
fetchRepoData() fetchRepoData()
} else if (!isAuth) {
// Handle case where we're not authenticated yet
console.log('🔍 useRepoData: Waiting for authentication...')
setIsLoading(true)
setError(null)
} }
return () => { return () => {
isMounted = false isMounted = false
} }
}, [repoId, octokit]) }, [repoId, octokit, isAuth])
return { repoData, isLoading, error } return { repoData, isLoading, error }
} }