Removed flickering and updated form entry

This commit is contained in:
NasSharaf 2025-09-02 00:35:20 -04:00
parent ce851ef883
commit 3a2a6dca7e
6 changed files with 294 additions and 181 deletions

View File

@ -1,7 +1,7 @@
// src/components/onboarding/configure-step/configure-step.tsx
'use client'
import { useOnboarding } from '@/components/onboarding/useOnboarding'
import { useOnboarding } from '@/components/onboarding/store'
import { useGQLClient } from '@/context'
import { useWallet } from '@/context/WalletContext'
import { Alert, AlertDescription } from '@workspace/ui/components/alert'
@ -53,19 +53,13 @@ export function ConfigureStep() {
const [isLoadingDeployers, setIsLoadingDeployers] = useState(true)
const [isLoadingOrgs, setIsLoadingOrgs] = useState(true)
// Form state
const [deployOption, setDeployOption] = useState<'auction' | 'lrn'>(
(formData.deploymentType as 'auction' | 'lrn') || 'lrn' // Default to LRN for simplicity
)
const [numberOfDeployers, setNumberOfDeployers] = useState<string>(
formData.deployerCount || '1'
)
const [maxPrice, setMaxPrice] = useState<string>(formData.maxPrice || '1000')
const [selectedLrn, setSelectedLrn] = useState<string>(
formData.selectedLrn || ''
)
// Form state - using local state since these aren't in the simplified store
const [deployOption, setDeployOption] = useState<'auction' | 'lrn'>('lrn') // Default to LRN for simplicity
const [numberOfDeployers, setNumberOfDeployers] = useState<string>('1')
const [maxPrice, setMaxPrice] = useState<string>('1000')
const [selectedLrn, setSelectedLrn] = useState<string>('')
const [selectedOrg, setSelectedOrg] = useState<string>(
formData.selectedOrg || ''
formData.organizationSlug || ''
)
const [envVars, setEnvVars] = useState<
{ key: string; value: string; environments: string[] }[]
@ -88,19 +82,8 @@ export function ConfigureStep() {
}
}, [mounted])
// Initialize environment variables from formData if available
useEffect(() => {
if (
formData.environmentVariables &&
Array.isArray(formData.environmentVariables)
) {
setEnvVars(
formData.environmentVariables.length > 0
? formData.environmentVariables
: [{ key: '', value: '', environments: ['Production'] }]
)
}
}, [formData.environmentVariables])
// Environment variables are managed locally
// (Removed environment variables initialization since not in simple store)
// Fetch deployers from backend
const fetchDeployers = async () => {
@ -231,12 +214,12 @@ export function ConfigureStep() {
// Save configuration to form data
setFormData({
deploymentType: deployOption,
deployerCount: numberOfDeployers,
maxPrice: maxPrice,
selectedLrn: selectedLrn,
organizationSlug: selectedOrg,
selectedOrg: selectedOrg,
paymentAddress: wallet?.address,
selectedLrn: selectedLrn,
deploymentType: deployOption,
maxPrice: maxPrice,
deployerCount: numberOfDeployers,
environmentVariables: validEnvVars
})
@ -251,11 +234,9 @@ export function ConfigureStep() {
// Determine if dark mode is active
const isDarkMode = resolvedTheme === 'dark'
// Get deployment mode info
const isTemplateMode = formData.deploymentMode === 'template'
const selectedItem = isTemplateMode
? formData.template?.name
: formData.githubRepo
// Get deployment mode info - determine from available data
const isTemplateMode = !!formData.framework && !formData.repoName
const selectedItem = isTemplateMode ? formData.framework : formData.repoName
return (
<div className="w-full h-full flex flex-col p-8 overflow-y-auto">

View File

@ -2,7 +2,7 @@
'use client'
import { GitHubBackendAuth } from '@/components/GitHubBackendAuth'
import { useOnboarding } from '@/components/onboarding/useOnboarding'
import { useOnboarding } from '@/components/onboarding/store'
import { AVAILABLE_TEMPLATES, type TemplateDetail } from '@/constants/templates'
import { useAuthStatus } from '@/hooks/useAuthStatus'
import { useRepoData } from '@/hooks/useRepoData'
@ -46,10 +46,12 @@ export function ConnectStep() {
// Repository vs Template selection
const [selectedRepo, setSelectedRepo] = useState<string>(
formData.githubRepo || ''
formData.repoName || ''
)
const [selectedTemplate, setSelectedTemplate] = useState(
adaptOptionalTemplate(formData.template)
const [selectedTemplate, setSelectedTemplate] = useState<
TemplateDetail | undefined
>(
undefined // We'll simplify template handling
)
const [projectName, setProjectName] = useState<string>(
formData.projectName || ''
@ -90,9 +92,10 @@ export function ConnectStep() {
setSelectedRepo(repo)
setSelectedTemplate(undefined)
setFormData({
githubRepo: repo,
template: undefined,
deploymentMode: 'repository',
repoName: repo,
githubRepo: repo, // Store repo path for deploy step
template: undefined, // Clear template selection
framework: '', // Clear framework
projectName
})
}
@ -107,9 +110,10 @@ export function ConnectStep() {
setProjectName(suggestedName)
}
setFormData({
template: template,
githubRepo: '',
deploymentMode: 'template',
framework: template.name, // Keep for backwards compatibility
template: template, // Store the full template object
githubRepo: '', // Clear repo selection
repoName: '',
projectName:
projectName ||
`my-${template.name.toLowerCase().replace(/[^a-z0-9]/g, '-')}`
@ -123,15 +127,17 @@ export function ConnectStep() {
if (mode === 'import') {
setSelectedTemplate(undefined)
setFormData({
framework: '',
template: undefined,
deploymentMode: 'repository',
githubRepo: '',
projectName
})
} else {
setSelectedRepo('')
setFormData({
repoName: '',
githubRepo: '',
deploymentMode: 'template',
template: undefined,
projectName
})
}
@ -186,9 +192,8 @@ export function ConnectStep() {
// Set final form data and proceed
setFormData({
deploymentMode: isImportMode ? 'repository' : 'template',
githubRepo: isImportMode ? selectedRepo : '',
template: !isImportMode ? (selectedTemplate as Template) : undefined,
repoName: isImportMode ? selectedRepo : '',
framework: !isImportMode ? selectedTemplate?.name || '' : '',
projectName: finalProjectName
})

View File

@ -18,6 +18,7 @@ import type { OnboardingFormData, Step } from './types'
* @property {(data: Partial<OnboardingFormData>) => void} setFormData - Updates form data
* @property {() => void} nextStep - Moves to the next step
* @property {() => void} previousStep - Moves to the previous step
* @property {() => void} resetOnboarding - Resets the onboarding state to initial values
*/
export interface OnboardingState {
currentStep: Step
@ -26,11 +27,34 @@ export interface OnboardingState {
setFormData: (data: Partial<OnboardingFormData>) => void
nextStep: () => void
previousStep: () => void
resetOnboarding: () => void
}
/** Order of steps in the onboarding flow */
const STEP_ORDER: Step[] = ['connect', 'configure', 'deploy']
/** Initial form data values */
const initialFormData: OnboardingFormData = {
projectName: '',
repoName: '',
repoDescription: '',
framework: '',
access: 'public',
organizationSlug: '',
template: undefined,
githubRepo: '',
selectedOrg: '',
environmentVariables: [],
selectedLrn: '',
deploymentType: 'lrn',
maxPrice: '1000',
deployerCount: '1',
deploymentId: undefined,
deploymentUrl: undefined,
projectId: undefined,
repositoryUrl: undefined
}
/**
* Zustand store for managing onboarding state
* Used across all onboarding components to maintain flow state
@ -42,14 +66,7 @@ const STEP_ORDER: Step[] = ['connect', 'configure', 'deploy']
*/
export const useOnboarding = create<OnboardingState>((set) => ({
currentStep: 'connect',
formData: {
projectName: '',
repoName: '',
repoDescription: '',
framework: '',
access: 'public',
organizationSlug: ''
},
formData: initialFormData,
setCurrentStep: (step) => set({ currentStep: step }),
setFormData: (data) =>
set((state) => ({
@ -66,5 +83,6 @@ export const useOnboarding = create<OnboardingState>((set) => ({
const currentIndex = STEP_ORDER.indexOf(state.currentStep)
const previousStep = STEP_ORDER[currentIndex - 1]
return previousStep ? { currentStep: previousStep } : state
})
}),
resetOnboarding: () => set({ currentStep: 'connect', formData: initialFormData })
}))

View File

@ -18,6 +18,18 @@ export type Step = 'connect' | 'configure' | 'deploy' | 'success'
* @property {string} framework - Framework used for the project
* @property {string} access - Access level of the repository
* @property {string} organizationSlug - Organization slug
* @property {Template | undefined} template - Selected template for deployment
* @property {string} githubRepo - GitHub repository path (owner/repo)
* @property {string} selectedOrg - Selected organization for deployment
* @property {EnvironmentVariable[]} environmentVariables - Environment variables for deployment
* @property {string} selectedLrn - Selected LRN for deployment
* @property {string} deploymentType - Type of deployment (lrn, wallet, etc.)
* @property {string} maxPrice - Maximum price for deployment
* @property {string} deployerCount - Number of deployers
* @property {string} deploymentId - ID of the deployment after creation
* @property {string} deploymentUrl - URL of the deployed project
* @property {string} projectId - ID of the created project
* @property {string} repositoryUrl - URL of the repository
*/
export interface OnboardingFormData {
projectName: string
@ -26,6 +38,18 @@ export interface OnboardingFormData {
framework: string
access: 'public' | 'private'
organizationSlug: string
template?: Template
githubRepo: string
selectedOrg: string
environmentVariables: EnvironmentVariable[]
selectedLrn: string
deploymentType: string
maxPrice: string
deployerCount: string
deploymentId?: string
deploymentUrl?: string
projectId?: string
repositoryUrl?: string
}
/**
@ -54,12 +78,16 @@ export interface Repository {
* @property {string} name - Template name
* @property {string} [description] - Template description
* @property {string} [thumbnail] - Template thumbnail URL
* @property {string} [repoFullName] - Full repository name for the template
* @property {any} [icon] - Template icon
*/
export interface Template {
id: string
name: string
description?: string
thumbnail?: string
repoFullName?: string
icon?: any
}
/**
@ -81,9 +109,11 @@ export interface DeploymentType {
* @property {string} key - Environment variable key
* @property {string} value - Environment variable value
* @property {boolean} [isSecret] - Whether the variable is a secret
* @property {string[]} environments - Environment names where this variable applies
*/
export interface EnvironmentVariable {
key: string
value: string
isSecret?: boolean
environments: string[]
}

View File

@ -1,11 +1,11 @@
// src/hooks/useAuthStatus.tsx
'use client'
import { useAuth, useUser } from '@clerk/nextjs'
import { useWallet } from '@/context/WalletContext' // Use the full provider!
import { useBackend } from '@/context/BackendContext'
import { useGQLClient } from '@/context'
import { useState, useEffect, useCallback } from 'react'
import { useBackend } from '@/context/BackendContext'
import { useWallet } from '@/context/WalletContext' // Use the full provider!
import { useAuth, useUser } from '@clerk/nextjs'
import { useCallback, useEffect, useRef, useState } from 'react'
/**
* @interface AuthStatus
@ -20,8 +20,8 @@ export interface AuthStatus {
user: any
}
wallet: {
isConnected: boolean // SIWE authenticated + backend session
hasAddress: boolean // Just has wallet address
isConnected: boolean // SIWE authenticated + backend session
hasAddress: boolean // Just has wallet address
wallet: any
}
backend: {
@ -29,11 +29,11 @@ export interface AuthStatus {
hasGithubAuth: boolean
isLoading: boolean
}
// Computed status
isFullyAuthenticated: boolean
isReady: boolean
// What's missing (for UI feedback)
missing: {
clerkSignIn: boolean
@ -42,7 +42,7 @@ export interface AuthStatus {
backendConnection: boolean
githubBackendSync: boolean
}
// Progress (for UI indicators)
progress: {
completed: number
@ -58,7 +58,7 @@ export interface AuthStatus {
export interface AuthActions {
// Wallet actions
connectWallet: () => Promise<void>
// Combined actions
refreshAllStatus: () => Promise<void>
checkGithubBackendAuth: () => Promise<boolean>
@ -73,35 +73,52 @@ export function useAuthStatus(): AuthStatus & AuthActions {
// Clerk authentication
const { isSignedIn, isLoaded: isClerkLoaded } = useAuth()
const { user, isLoaded: isUserLoaded } = useUser()
// Wallet authentication
const {
isConnected: isWalletSessionActive, // SIWE authenticated
hasWalletAddress,
wallet,
connect: connectWallet
const {
isConnected: isWalletSessionActive, // SIWE authenticated
hasWalletAddress,
wallet,
connect: connectWallet
} = useWallet()
// Backend authentication
const {
isBackendConnected,
isLoading: isBackendLoading,
refreshStatus: refreshBackendStatus
} = useBackend()
// GraphQL client for checking GitHub backend auth
const gqlClient = useGQLClient()
// GitHub backend auth state
const [isGithubBackendAuth, setIsGithubBackendAuth] = useState(false)
const [isCheckingGithubAuth, setIsCheckingGithubAuth] = useState(false)
const lastGithubCheckRef = useRef(0)
const isCheckingRef = useRef(false)
// Stable status to prevent rapid UI changes
const [stableAuthStatus, setStableAuthStatus] = useState({
isFullyAuthenticated: false,
lastUpdate: 0
})
// Check GitHub backend auth via GraphQL
const checkGithubBackendAuth = useCallback(async (): Promise<boolean> => {
if (!isBackendConnected) return false
// Prevent multiple rapid calls - only allow once every 3 seconds
const now = Date.now()
if (isCheckingRef.current || (now - lastGithubCheckRef.current < 3000)) {
return isGithubBackendAuth
}
try {
isCheckingRef.current = true
setIsCheckingGithubAuth(true)
lastGithubCheckRef.current = now
const userData = await gqlClient.getUser()
const hasGitHubToken = !!userData.user.gitHubToken
setIsGithubBackendAuth(hasGitHubToken)
@ -111,10 +128,11 @@ export function useAuthStatus(): AuthStatus & AuthActions {
setIsGithubBackendAuth(false)
return false
} finally {
isCheckingRef.current = false
setIsCheckingGithubAuth(false)
}
}, [isBackendConnected, gqlClient])
}, [isBackendConnected, isGithubBackendAuth]) // Minimal dependencies
// Check GitHub auth when backend connection changes
useEffect(() => {
if (isBackendConnected) {
@ -122,9 +140,9 @@ export function useAuthStatus(): AuthStatus & AuthActions {
} else {
setIsGithubBackendAuth(false)
}
}, [isBackendConnected, checkGithubBackendAuth])
// Check backend connection when wallet session is active (SIWE completed)
}, [isBackendConnected]) // Remove checkGithubBackendAuth from dependencies to prevent infinite loop
// Check backend connection when wallet session is active (SIWE completed)
useEffect(() => {
if (isWalletSessionActive) {
// Wait a moment for wallet session to be established, then check backend
@ -133,13 +151,16 @@ export function useAuthStatus(): AuthStatus & AuthActions {
}, 1000)
return () => clearTimeout(timer)
}
}, [isWalletSessionActive, refreshBackendStatus])
}, [isWalletSessionActive]) // Remove refreshBackendStatus from dependencies to prevent rapid retriggers
// Check if GitHub is connected in Clerk
const hasGithubInClerk = user?.externalAccounts?.find(
account => account.provider === 'github' || account.verification?.strategy === 'oauth_github'
) !== undefined
const hasGithubInClerk =
user?.externalAccounts?.find(
(account) =>
account.provider === 'github' ||
account.verification?.strategy === 'oauth_github'
) !== undefined
// Calculate what's missing
const missing = {
clerkSignIn: !isSignedIn,
@ -148,32 +169,50 @@ export function useAuthStatus(): AuthStatus & AuthActions {
backendConnection: hasWalletAddress && !isWalletSessionActive, // Need SIWE auth for backend
githubBackendSync: isBackendConnected && !isGithubBackendAuth
}
// Calculate progress
const authSteps = [
isSignedIn, // Clerk sign in
hasGithubInClerk, // GitHub connected to Clerk
hasWalletAddress, // Wallet address obtained
isWalletSessionActive, // SIWE authentication completed
isGithubBackendAuth // GitHub synced to backend
isSignedIn, // Clerk sign in
hasGithubInClerk, // GitHub connected to Clerk
hasWalletAddress, // Wallet address obtained
isWalletSessionActive, // SIWE authentication completed
isGithubBackendAuth // GitHub synced to backend
]
const completedSteps = authSteps.filter(Boolean).length
const totalSteps = authSteps.length
const progressPercentage = Math.round((completedSteps / totalSteps) * 100)
// Determine if fully authenticated
const isFullyAuthenticated = authSteps.every(Boolean)
const currentIsFullyAuthenticated = authSteps.every(Boolean)
// Debounce authentication status changes to prevent flickering
useEffect(() => {
const now = Date.now()
const timeSinceLastUpdate = now - stableAuthStatus.lastUpdate
// Only update if status actually changed and enough time has passed (300ms debounce)
if (currentIsFullyAuthenticated !== stableAuthStatus.isFullyAuthenticated && timeSinceLastUpdate > 300) {
setStableAuthStatus({
isFullyAuthenticated: currentIsFullyAuthenticated,
lastUpdate: now
})
}
}, [currentIsFullyAuthenticated, stableAuthStatus])
// Use stable status for UI
const isFullyAuthenticated = stableAuthStatus.isFullyAuthenticated
// Determine if ready (all auth systems loaded)
const isReady = isClerkLoaded && isUserLoaded && !isBackendLoading && !isCheckingGithubAuth
const isReady =
isClerkLoaded && isUserLoaded && !isBackendLoading && !isCheckingGithubAuth
// Combined refresh action
const refreshAllStatus = async () => {
await refreshBackendStatus()
await checkGithubBackendAuth()
}
return {
// Individual systems
clerk: {
@ -192,24 +231,24 @@ export function useAuthStatus(): AuthStatus & AuthActions {
hasGithubAuth: isGithubBackendAuth,
isLoading: isBackendLoading || isCheckingGithubAuth
},
// Computed status
isFullyAuthenticated,
isReady,
// Missing items
missing,
// Progress
progress: {
completed: completedSteps,
total: totalSteps,
percentage: progressPercentage
},
// Actions
connectWallet,
refreshAllStatus,
checkGithubBackendAuth
}
}
}

View File

@ -1,139 +1,179 @@
"use client";
'use client'
import { useState, useEffect } from "react";
import { useAuth, useUser } from "@clerk/nextjs";
import { Octokit } from "@octokit/rest";
import { getGitHubToken } from "@/actions/github";
import { getGitHubToken } from '@/actions/github'
import { useAuth, useUser } from '@clerk/nextjs'
import { Octokit } from '@octokit/rest'
import { useEffect, useState } from 'react'
// Define the return type of the hook
interface UseRepoDataReturn {
repoData: any;
isLoading: boolean;
error: string | null;
repoData: any
isLoading: boolean
error: string | null
}
/**
* A hook to fetch repository data from GitHub
*
*
* @param repoId - The GitHub repository ID to fetch, or empty string to fetch all repos
* @returns Object containing repository data, loading state, and any errors
*/
export function useRepoData(repoId: string): UseRepoDataReturn {
const [repoData, setRepoData] = useState<any>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [octokit, setOctokit] = useState<Octokit | null>(null);
const [repoData, setRepoData] = useState<any>(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [octokit, setOctokit] = useState<Octokit | null>(null)
// Get auth data from Clerk
const { isLoaded: isAuthLoaded } = useAuth();
const { isLoaded: isUserLoaded, user } = useUser();
const { isLoaded: isAuthLoaded, getToken, isSignedIn } = useAuth()
const { isLoaded: isUserLoaded, user } = useUser()
// Initialize Octokit with the appropriate token
useEffect(() => {
async function initializeOctokit() {
let token = null;
// Try to get GitHub token from Clerk
if (user) {
let token = null
// Try to get GitHub OAuth token from Clerk using multiple methods
if (isSignedIn && user) {
try {
// Check if user has connected GitHub account
const githubAccount = user.externalAccounts.find(
account => account.provider === 'github'
);
(account) => account.provider === 'github'
)
if (githubAccount) {
// Try to get GitHub OAuth token from Clerk
// Try multiple methods to get the GitHub OAuth token (same as test-connection page)
// Method 1: Try getToken from useAuth hook
try {
token = await getGitHubToken();
console.log('Using GitHub token from Clerk');
} catch (err) {
console.error('Error getting GitHub token from Clerk:', err);
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);
console.error('Error accessing Clerk user data:', err)
}
}
// Fallback to token from environment variable
if (!token && typeof process !== 'undefined') {
token = process.env.NEXT_PUBLIC_GITHUB_FALLBACK_TOKEN || '';
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.');
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 }));
setOctokit(new Octokit({ auth: token }))
} else {
setError("No GitHub token available");
setIsLoading(false);
setError('No GitHub token available')
setIsLoading(false)
}
}
if (isAuthLoaded && isUserLoaded) {
initializeOctokit();
initializeOctokit()
}
}, [isAuthLoaded, isUserLoaded, user]);
}, [isAuthLoaded, isUserLoaded, user])
// Fetch repo data when Octokit is available
useEffect(() => {
let isMounted = true;
let isMounted = true
async function fetchRepoData() {
if (!octokit) {
return;
return
}
try {
// Fetch repos from GitHub
const { data: repos } = await octokit.repos.listForAuthenticatedUser();
const { data: repos } = await octokit.repos.listForAuthenticatedUser()
// If no repoId is provided, return all repos
if (!repoId) {
if (isMounted) {
setRepoData(repos);
setError(null);
setIsLoading(false);
setRepoData(repos)
setError(null)
setIsLoading(false)
}
return;
return
}
// Find the specific repo by ID if repoId is provided
const repo = repos.find(repo => repo.id.toString() === repoId);
const repo = repos.find((repo) => repo.id.toString() === repoId)
if (!repo) {
if (isMounted) {
setError("Repository not found");
setRepoData(null);
setIsLoading(false);
setError('Repository not found')
setRepoData(null)
setIsLoading(false)
}
} else {
if (isMounted) {
setRepoData(repo);
setError(null);
setIsLoading(false);
setRepoData(repo)
setError(null)
setIsLoading(false)
}
}
} catch (err) {
console.error('Error fetching GitHub repo:', err);
console.error('Error fetching GitHub repo:', err)
if (isMounted) {
setError('Failed to fetch repository data');
setRepoData(null);
setIsLoading(false);
setError('Failed to fetch repository data')
setRepoData(null)
setIsLoading(false)
}
}
}
if (octokit) {
fetchRepoData();
}
return () => {
isMounted = false;
};
}, [repoId, octokit]);
return { repoData, isLoading, error };
}
if (octokit) {
fetchRepoData()
}
return () => {
isMounted = false
}
}, [repoId, octokit])
return { repoData, isLoading, error }
}