Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
895c04de03 | ||
|
|
f0a7cb97b4 | ||
|
|
685f8a8793 | ||
|
|
1445cb865b | ||
|
|
cd3cd7667a | ||
|
|
3a2a6dca7e | ||
|
|
ce851ef883 |
@@ -3,4 +3,4 @@ CLERK_SECRET_KEY=
|
||||
NEXT_PUBLIC_WALLET_IFRAME_URL= # wherever your wallet is running
|
||||
NEXT_PUBLIC_LACONICD_CHAIN_ID= # the appropriate chain ID for your network
|
||||
NEXT_PUBLIC_API_URL=
|
||||
NEXT_PUBLIC_GITHUB_FALLBACK_TOKEN=
|
||||
NEXT_PUBLIC_GITHUB_BACKEND_CLIENT_ID= # Client ID of your GitHub OAuth App
|
||||
|
||||
@@ -34,3 +34,30 @@ export async function getGitHubOrgs() {
|
||||
avatarUrl: org.avatar_url
|
||||
}))
|
||||
}
|
||||
|
||||
export async function getGitHubToken() {
|
||||
const { userId } = await auth()
|
||||
|
||||
if (!userId) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
|
||||
const user = await currentUser()
|
||||
const githubAccount = user?.externalAccounts.find(
|
||||
(account) => account.provider === 'github'
|
||||
)
|
||||
|
||||
if (!githubAccount) {
|
||||
throw new Error('GitHub not connected')
|
||||
}
|
||||
|
||||
// For server actions, we can access the external account token directly
|
||||
// This is a simplified approach that uses the account's external ID as token
|
||||
const token = githubAccount.externalId
|
||||
|
||||
if (!token) {
|
||||
throw new Error('Failed to get GitHub token')
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
+3
-3
@@ -5,7 +5,7 @@ import { ConfigureStep } from '@/components/onboarding/configure-step/configure-
|
||||
import { ConnectStep } from '@/components/onboarding/connect-step/connect-step'
|
||||
import { DeployStep } from '@/components/onboarding/deploy-step/deploy-step'
|
||||
import { SuccessStep } from '@/components/onboarding/success-step/success-step'
|
||||
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
||||
import { useOnboarding } from '@/components/onboarding/store'
|
||||
import { X } from 'lucide-react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useRouter } from 'next/navigation'
|
||||
@@ -20,7 +20,7 @@ export default function CreateProjectFlow() {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
const { currentStep, setCurrentStep, resetOnboarding } = useOnboarding()
|
||||
const { currentStep, setCurrentStep } = useOnboarding()
|
||||
|
||||
// Handle hydration mismatch by waiting for mount
|
||||
useEffect(() => {
|
||||
@@ -32,7 +32,7 @@ export default function CreateProjectFlow() {
|
||||
return () => {
|
||||
// Optional cleanup actions
|
||||
}
|
||||
}, [resetOnboarding])
|
||||
}, [])
|
||||
|
||||
// Handle closing the modal
|
||||
const handleClose = () => {
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
import { PageWrapper } from '@/components/foundation'
|
||||
import CheckBalanceIframe from '@/components/iframe/check-balance-iframe/CheckBalanceIframe'
|
||||
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 { useUser } from '@clerk/nextjs'
|
||||
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 {
|
||||
id: string
|
||||
@@ -23,55 +24,104 @@ export default function ProjectsPage() {
|
||||
const [projects, setProjects] = useState<ProjectData[]>([])
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
|
||||
const client = useGQLClient()
|
||||
const { user } = useUser()
|
||||
|
||||
const handleCreateProject = () => {
|
||||
window.location.href = '/projects/github/ps/cr'
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadAllProjects()
|
||||
}, [])
|
||||
if (user !== undefined) {
|
||||
loadAllProjects()
|
||||
}
|
||||
}, [user])
|
||||
|
||||
const loadAllProjects = async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
// First get organizations
|
||||
const orgsResponse = await client.getOrganizations()
|
||||
|
||||
if (!orgsResponse.organizations || orgsResponse.organizations.length === 0) {
|
||||
|
||||
if (user === null) {
|
||||
// User is not authenticated
|
||||
setProjects([])
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Get the authenticated user's GitHub username
|
||||
const githubAccount = user?.externalAccounts?.find(
|
||||
(account) => account.provider === 'github'
|
||||
)
|
||||
const githubUsername = githubAccount?.username
|
||||
|
||||
if (!githubUsername) {
|
||||
console.warn('No GitHub username found for user')
|
||||
setProjects([])
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
console.log('🔍 Filtering projects for GitHub user:', githubUsername)
|
||||
|
||||
// First get organizations
|
||||
const orgsResponse = await client.getOrganizations()
|
||||
|
||||
if (
|
||||
!orgsResponse.organizations ||
|
||||
orgsResponse.organizations.length === 0
|
||||
) {
|
||||
setProjects([])
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Get projects from all organizations
|
||||
const allProjects: ProjectData[] = []
|
||||
|
||||
|
||||
for (const org of orgsResponse.organizations) {
|
||||
try {
|
||||
const projectsResponse = await client.getProjectsInOrganization(org.slug)
|
||||
|
||||
// Transform GraphQL projects to match ProjectData interface
|
||||
const orgProjects: ProjectData[] = projectsResponse.projectsInOrganization.map((project: Project) => ({
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
repository: project.repository,
|
||||
framework: project.framework,
|
||||
description: project.description,
|
||||
deployments: project.deployments || []
|
||||
}))
|
||||
|
||||
allProjects.push(...orgProjects)
|
||||
const projectsResponse = await client.getProjectsInOrganization(
|
||||
org.slug
|
||||
)
|
||||
|
||||
// Filter projects by GitHub username and transform to ProjectData interface
|
||||
const userProjects: ProjectData[] =
|
||||
projectsResponse.projectsInOrganization
|
||||
.filter((project: Project) => {
|
||||
if (project.repository) {
|
||||
const repoOwner = project.repository.split('/')[0]
|
||||
console.log(
|
||||
`🔍 Project ${project.name}: repo owner = ${repoOwner}, current user = ${githubUsername}`
|
||||
)
|
||||
return repoOwner.toLowerCase() === githubUsername.toLowerCase()
|
||||
}
|
||||
return true // Include projects without repository info
|
||||
})
|
||||
.map((project: Project) => ({
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
repository: project.repository,
|
||||
framework: project.framework,
|
||||
description: project.description,
|
||||
deployments: project.deployments || []
|
||||
}))
|
||||
|
||||
console.log(
|
||||
`🔍 Found ${userProjects.length} projects for ${githubUsername} in org ${org.slug}`
|
||||
)
|
||||
allProjects.push(...userProjects)
|
||||
} catch (orgError) {
|
||||
console.error(`Failed to load projects for org ${org.slug}:`, orgError)
|
||||
console.error(
|
||||
`Failed to load projects for org ${org.slug}:`,
|
||||
orgError
|
||||
)
|
||||
// Continue with other orgs even if one fails
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
console.log('🔍 Total filtered projects:', allProjects)
|
||||
setProjects(allProjects)
|
||||
} catch (err) {
|
||||
console.error('Failed to load projects:', err)
|
||||
@@ -80,7 +130,7 @@ export default function ProjectsPage() {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<PageWrapper
|
||||
header={{
|
||||
@@ -107,7 +157,7 @@ export default function ProjectsPage() {
|
||||
<p className="text-gray-400 text-center max-w-md mb-6">
|
||||
Failed to load your deployed projects. Please try again.
|
||||
</p>
|
||||
<Button
|
||||
<Button
|
||||
className="bg-white text-black hover:bg-gray-200 flex items-center"
|
||||
onClick={loadAllProjects}
|
||||
>
|
||||
@@ -124,13 +174,20 @@ export default function ProjectsPage() {
|
||||
</div>
|
||||
<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">
|
||||
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>
|
||||
<Button
|
||||
<Button
|
||||
className="bg-white text-black hover:bg-gray-200 flex items-center"
|
||||
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="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
@@ -143,9 +200,11 @@ export default function ProjectsPage() {
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{projects.map((project) => {
|
||||
// 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
|
||||
|
||||
|
||||
// Determine status based on deployment
|
||||
let status = 'pending'
|
||||
if (currentDeployment || latestDeployment) {
|
||||
@@ -167,30 +226,34 @@ export default function ProjectsPage() {
|
||||
status = 'pending'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Format the project data to match what FixedProjectCard expects
|
||||
const formattedProject = {
|
||||
id: project.id,
|
||||
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,
|
||||
framework: project.framework,
|
||||
description: project.description,
|
||||
// Ensure deployments array is properly formatted
|
||||
deployments: project.deployments.map(deployment => ({
|
||||
deployments: project.deployments.map((deployment) => ({
|
||||
...deployment,
|
||||
// Make sure the date is in a format the card can parse
|
||||
createdAt: deployment.createdAt,
|
||||
applicationDeploymentRecordData: {
|
||||
url: deployment.applicationDeploymentRecordData?.url || `https://${project.name.toLowerCase()}.example.com`
|
||||
url:
|
||||
deployment.applicationDeploymentRecordData?.url ||
|
||||
`https://${project.name.toLowerCase()}.example.com`
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<FixedProjectCard
|
||||
project={formattedProject}
|
||||
key={project.id}
|
||||
<FixedProjectCard
|
||||
project={formattedProject}
|
||||
key={project.id}
|
||||
status={status as any}
|
||||
/>
|
||||
)
|
||||
@@ -198,7 +261,7 @@ export default function ProjectsPage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* Wrap in try/catch to prevent breaking if there are issues */}
|
||||
{(() => {
|
||||
try {
|
||||
@@ -208,12 +271,12 @@ export default function ProjectsPage() {
|
||||
isPollingEnabled={false}
|
||||
amount="1"
|
||||
/>
|
||||
);
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Failed to render CheckBalanceIframe:', error);
|
||||
return null;
|
||||
console.error('Failed to render CheckBalanceIframe:', error)
|
||||
return null
|
||||
}
|
||||
})()}
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 || ''
|
||||
@@ -71,7 +73,17 @@ export function ConnectStep() {
|
||||
} = useAuthStatus()
|
||||
|
||||
// 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
|
||||
useEffect(() => {
|
||||
@@ -90,9 +102,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 +120,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 +137,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 +202,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
|
||||
})
|
||||
|
||||
|
||||
@@ -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 })
|
||||
}))
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRouter, useParams } from 'next/navigation'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { CheckCircle } from 'lucide-react'
|
||||
import { useOnboarding } from '@/components/onboarding/store'
|
||||
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() {
|
||||
const router = useRouter()
|
||||
@@ -14,45 +14,49 @@ export function SuccessStep() {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const providerParam = params?.provider ? String(params.provider) : 'github'
|
||||
const { formData, resetOnboarding } = useOnboarding()
|
||||
|
||||
|
||||
// Handle hydration mismatch by waiting for mount
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
// Get deployment info from form data
|
||||
const repoName = formData.githubRepo ? formData.githubRepo.split('/').pop() : (formData.projectName || 'project')
|
||||
const deploymentUrl = formData.deploymentUrl || `https://${repoName}.laconic.deploy`
|
||||
const projectId = formData.projectId || 'unknown-id'
|
||||
|
||||
|
||||
// Get deployment info from form data - using available properties
|
||||
const repoName = formData.githubRepo
|
||||
? formData.githubRepo.split('/').pop()
|
||||
: 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
|
||||
const handleViewProject = () => {
|
||||
console.log('Navigating to project with ID:', projectId)
|
||||
resetOnboarding() // Reset state for next time
|
||||
|
||||
|
||||
// Navigate to the project detail page using the GraphQL project ID
|
||||
router.push(`/projects/${providerParam}/ps/${projectId}`)
|
||||
}
|
||||
|
||||
|
||||
// Auto-redirect after a delay (optional)
|
||||
useEffect(() => {
|
||||
if (mounted && projectId && projectId !== 'unknown-id') {
|
||||
const timer = setTimeout(() => {
|
||||
handleViewProject()
|
||||
}, 3000) // Auto-redirect after 3 seconds
|
||||
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [mounted, projectId])
|
||||
|
||||
|
||||
// Don't render UI until after mount to prevent hydration mismatch
|
||||
if (!mounted) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
// Determine if dark mode is active
|
||||
const isDarkMode = resolvedTheme === 'dark'
|
||||
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center p-8">
|
||||
<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">
|
||||
<CheckCircle className="h-16 w-16 text-green-500" />
|
||||
</div>
|
||||
|
||||
|
||||
{/* 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!
|
||||
</h2>
|
||||
<p className="text-center text-zinc-500 mb-8">
|
||||
Your project has been deployed successfully
|
||||
</p>
|
||||
|
||||
|
||||
{/* 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="flex justify-between">
|
||||
<span className="text-muted-foreground">Project:</span>
|
||||
@@ -86,39 +94,82 @@ export function SuccessStep() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Next steps section */}
|
||||
<div className="mb-8">
|
||||
<h3 className={`text-lg font-medium ${isDarkMode ? "text-white" : "text-zinc-900"} mb-4`}>Next steps</h3>
|
||||
|
||||
<div className={`border rounded-md overflow-hidden mb-4 ${isDarkMode ? "border-zinc-700" : "border-zinc-300"}`}>
|
||||
<h3
|
||||
className={`text-lg font-medium ${isDarkMode ? 'text-white' : 'text-zinc-900'} mb-4`}
|
||||
>
|
||||
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>
|
||||
<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
|
||||
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>
|
||||
<Button variant="outline" 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"/>
|
||||
<Button
|
||||
variant="outline"
|
||||
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>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Action buttons */}
|
||||
<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"
|
||||
onClick={handleViewProject}
|
||||
disabled={!projectId || projectId === 'unknown-id'}
|
||||
>
|
||||
View Project
|
||||
<svg 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"/>
|
||||
View Project
|
||||
<svg
|
||||
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>
|
||||
</Button>
|
||||
|
||||
|
||||
{/* Manual navigation button if auto-redirect fails */}
|
||||
{projectId === 'unknown-id' && (
|
||||
<p className="text-center text-xs text-muted-foreground">
|
||||
@@ -129,4 +180,4 @@ export function SuccessStep() {
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
'use client'
|
||||
|
||||
import { getGitHubToken } from '@/actions/github'
|
||||
import { Octokit } from '@octokit/rest'
|
||||
import { useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
interface CreateRepoFromTemplateParams {
|
||||
templateOwner: string
|
||||
templateRepo: string
|
||||
name: string
|
||||
description?: string
|
||||
isPrivate?: boolean
|
||||
}
|
||||
|
||||
interface CreateRepoResult {
|
||||
success: boolean
|
||||
repositoryUrl?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to directly interact with GitHub API using user's own token
|
||||
* Bypasses backend GitHub integration issues
|
||||
*/
|
||||
export function useDirectGitHub() {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const createRepoFromTemplate = async (
|
||||
params: CreateRepoFromTemplateParams
|
||||
): Promise<CreateRepoResult> => {
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
console.log(
|
||||
'🔄 Creating repository from template directly via GitHub API...'
|
||||
)
|
||||
console.log('📋 Parameters:', params)
|
||||
|
||||
// Get user's GitHub token
|
||||
const token = await getGitHubToken()
|
||||
if (!token) {
|
||||
throw new Error(
|
||||
'GitHub token not available. Please reconnect your GitHub account.'
|
||||
)
|
||||
}
|
||||
|
||||
// Create Octokit instance with user's token
|
||||
const octokit = new Octokit({ auth: token })
|
||||
|
||||
// Get the authenticated user's info to use as owner
|
||||
const { data: authUser } = await octokit.rest.users.getAuthenticated()
|
||||
console.log('👤 Authenticated GitHub user:', authUser.login)
|
||||
|
||||
// Create repository from template
|
||||
const { data: newRepo } = await octokit.rest.repos.createUsingTemplate({
|
||||
template_owner: params.templateOwner,
|
||||
template_repo: params.templateRepo,
|
||||
owner: authUser.login, // Use authenticated user as owner
|
||||
name: params.name,
|
||||
description:
|
||||
params.description ||
|
||||
`Created from ${params.templateOwner}/${params.templateRepo}`,
|
||||
private: params.isPrivate || false,
|
||||
include_all_branches: false
|
||||
})
|
||||
|
||||
console.log('✅ Repository created successfully:', newRepo.html_url)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
repositoryUrl: newRepo.html_url
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to create repository from template:', error)
|
||||
|
||||
let errorMessage = 'Failed to create repository from template'
|
||||
if (error instanceof Error) {
|
||||
errorMessage = error.message
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getUserRepos = async () => {
|
||||
try {
|
||||
const token = await getGitHubToken()
|
||||
if (!token) {
|
||||
throw new Error('GitHub token not available')
|
||||
}
|
||||
|
||||
const octokit = new Octokit({ auth: token })
|
||||
const { data: repos } = await octokit.rest.repos.listForAuthenticatedUser(
|
||||
{
|
||||
sort: 'updated',
|
||||
per_page: 100
|
||||
}
|
||||
)
|
||||
|
||||
return repos
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to fetch user repositories:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
createRepoFromTemplate,
|
||||
getUserRepos,
|
||||
isLoading
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
// src/hooks/useDirectTemplateDeployment.tsx
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useGQLClient } from '@/context'
|
||||
import { useWallet } from '@/context/WalletContext'
|
||||
import { useUser } from '@clerk/nextjs'
|
||||
import { toast } from 'sonner'
|
||||
import type { TemplateDetail } from '@/constants/templates'
|
||||
import { useDirectGitHub } from './useDirectGitHub'
|
||||
import { getGitHubToken } from '@/actions/github'
|
||||
|
||||
export interface TemplateDeploymentConfig {
|
||||
template: TemplateDetail
|
||||
projectName: string
|
||||
organizationSlug: string
|
||||
environmentVariables?: Array<{
|
||||
key: string
|
||||
value: string
|
||||
environments: string[]
|
||||
}>
|
||||
deployerLrn?: string
|
||||
}
|
||||
|
||||
export interface TemplateDeploymentResult {
|
||||
projectId: string
|
||||
repositoryUrl: string
|
||||
deploymentUrl?: string
|
||||
deploymentId?: string
|
||||
}
|
||||
|
||||
export function useDirectTemplateDeployment() {
|
||||
const [isDeploying, setIsDeploying] = useState(false)
|
||||
const [deploymentResult, setDeploymentResult] =
|
||||
useState<TemplateDeploymentResult | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const gqlClient = useGQLClient()
|
||||
const { wallet } = useWallet()
|
||||
const { user } = useUser()
|
||||
const directGitHub = useDirectGitHub()
|
||||
|
||||
const deployTemplate = async (
|
||||
config: TemplateDeploymentConfig
|
||||
): Promise<TemplateDeploymentResult> => {
|
||||
setIsDeploying(true)
|
||||
setError(null)
|
||||
setDeploymentResult(null)
|
||||
|
||||
try {
|
||||
console.log('🚀 Starting direct template deployment:', config)
|
||||
|
||||
// Validate required data
|
||||
if (!wallet?.address) {
|
||||
throw new Error('Wallet not connected')
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new Error('User not authenticated')
|
||||
}
|
||||
|
||||
// Get GitHub username from Clerk external accounts
|
||||
const githubAccount = user.externalAccounts?.find(
|
||||
(account) => account.provider === 'github'
|
||||
)
|
||||
const githubUsername = githubAccount?.username
|
||||
|
||||
if (!githubUsername) {
|
||||
throw new Error('GitHub account not connected')
|
||||
}
|
||||
|
||||
console.log('🔍 GitHub user info:', {
|
||||
githubUsername,
|
||||
githubAccount: githubAccount?.username,
|
||||
userExternalAccounts: user.externalAccounts?.length
|
||||
})
|
||||
|
||||
// Parse template repository (format: "owner/repo")
|
||||
const [templateOwner, templateRepo] =
|
||||
config.template.repoFullName.split('/')
|
||||
if (!templateOwner || !templateRepo) {
|
||||
throw new Error('Invalid template repository format')
|
||||
}
|
||||
|
||||
console.log('🔍 Template parsing details:', {
|
||||
originalTemplate: config.template.repoFullName,
|
||||
parsedOwner: templateOwner,
|
||||
parsedRepo: templateRepo,
|
||||
templateId: config.template.id,
|
||||
templateName: config.template.name
|
||||
})
|
||||
|
||||
toast.info('Creating repository from template...')
|
||||
|
||||
// STEP 1: Create repository directly via GitHub API with user's token
|
||||
const repoResult = await directGitHub.createRepoFromTemplate({
|
||||
templateOwner,
|
||||
templateRepo,
|
||||
name: config.projectName,
|
||||
description: `Created from ${config.template.name} template`,
|
||||
isPrivate: false
|
||||
})
|
||||
|
||||
if (!repoResult.success || !repoResult.repositoryUrl) {
|
||||
throw new Error(
|
||||
repoResult.error || 'Failed to create repository from template'
|
||||
)
|
||||
}
|
||||
|
||||
console.log(
|
||||
'✅ Repository created successfully:',
|
||||
repoResult.repositoryUrl
|
||||
)
|
||||
toast.success('Repository created from template!')
|
||||
|
||||
// STEP 2: Create project in backend using the newly created repository
|
||||
console.log('🔍 Preparing backend project creation...')
|
||||
console.log('🔍 Organization slug:', config.organizationSlug)
|
||||
console.log('🔍 Deployer LRN:', config.deployerLrn || 'undefined')
|
||||
console.log(
|
||||
'🔍 Environment variables:',
|
||||
config.environmentVariables || []
|
||||
)
|
||||
|
||||
toast.info('Setting up project deployment...')
|
||||
|
||||
// Add a delay to ensure the repository is fully created and accessible
|
||||
console.log('⏳ Waiting for repository to be fully accessible...')
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000))
|
||||
|
||||
// STEP 2a: Verify GitHub token availability
|
||||
console.log('🔄 Verifying GitHub token for user:', githubUsername)
|
||||
const currentClerkToken = await getGitHubToken()
|
||||
if (!currentClerkToken) {
|
||||
throw new Error(
|
||||
`GitHub token not available for user ${githubUsername}. Please reconnect your GitHub account.`
|
||||
)
|
||||
}
|
||||
console.log('✅ GitHub token verified for user:', githubUsername)
|
||||
|
||||
try {
|
||||
// Get available deployers
|
||||
console.log('🔍 Fetching available deployers...')
|
||||
const deployersResult = await gqlClient.getDeployers()
|
||||
const availableDeployers = deployersResult.deployers || []
|
||||
|
||||
if (availableDeployers.length === 0) {
|
||||
throw new Error(
|
||||
'No deployers available. Please configure at least one deployer in the backend.'
|
||||
)
|
||||
}
|
||||
|
||||
// Use the first available deployer if none specified
|
||||
const deployerToUse =
|
||||
config.deployerLrn || availableDeployers[0]?.deployerLrn
|
||||
|
||||
if (!deployerToUse) {
|
||||
throw new Error('No valid deployer found')
|
||||
}
|
||||
|
||||
console.log('🔍 Using deployer:', deployerToUse)
|
||||
|
||||
// Get the backend's wallet address for blockchain transactions
|
||||
const backendAddress = await gqlClient.getAddress()
|
||||
console.log('🔍 Backend wallet address:', backendAddress)
|
||||
console.log('🔍 Frontend wallet address:', wallet.address)
|
||||
|
||||
// Use backend's address for blockchain transactions
|
||||
const projectData = {
|
||||
name: config.projectName,
|
||||
repository: `${githubUsername}/${config.projectName}`,
|
||||
prodBranch: 'main',
|
||||
template: config.template.id,
|
||||
paymentAddress: backendAddress,
|
||||
txHash:
|
||||
'0x0000000000000000000000000000000000000000000000000000000000000000'
|
||||
}
|
||||
|
||||
console.log(
|
||||
'📤 Final project data being sent:',
|
||||
JSON.stringify(projectData, null, 2)
|
||||
)
|
||||
console.log('📤 With deployer:', deployerToUse)
|
||||
console.log('📤 Organization slug:', config.organizationSlug)
|
||||
console.log(
|
||||
'📤 Environment variables:',
|
||||
config.environmentVariables || []
|
||||
)
|
||||
|
||||
// Log the exact GraphQL variables being sent
|
||||
const mutationVariables = {
|
||||
organizationSlug: config.organizationSlug,
|
||||
data: projectData,
|
||||
lrn: deployerToUse,
|
||||
auctionParams: undefined,
|
||||
environmentVariables: config.environmentVariables || []
|
||||
}
|
||||
|
||||
console.log(
|
||||
'🔍 EXACT GraphQL variables being sent:',
|
||||
JSON.stringify(mutationVariables, null, 2)
|
||||
)
|
||||
|
||||
const projectResult = await gqlClient.addProject(
|
||||
config.organizationSlug,
|
||||
projectData,
|
||||
deployerToUse,
|
||||
undefined, // auctionParams
|
||||
config.environmentVariables || []
|
||||
)
|
||||
|
||||
console.log('✅ Backend response received:', projectResult)
|
||||
console.log('🔍 Project ID:', projectResult.addProject?.id)
|
||||
console.log('🔍 Full project object:', projectResult.addProject)
|
||||
|
||||
if (!projectResult.addProject?.id) {
|
||||
console.error(
|
||||
'❌ No project ID in response. Full response:',
|
||||
projectResult
|
||||
)
|
||||
throw new Error(
|
||||
'Failed to set up project deployment in backend - no project ID returned'
|
||||
)
|
||||
}
|
||||
|
||||
console.log(
|
||||
'✅ Project created successfully with ID:',
|
||||
projectResult.addProject.id
|
||||
)
|
||||
|
||||
// Create and return the result
|
||||
const result: TemplateDeploymentResult = {
|
||||
projectId: projectResult.addProject.id,
|
||||
repositoryUrl: repoResult.repositoryUrl,
|
||||
deploymentUrl: undefined, // Will be populated once deployment completes
|
||||
deploymentId: projectResult.addProject.id
|
||||
}
|
||||
|
||||
setDeploymentResult(result)
|
||||
toast.success('Template deployed successfully!')
|
||||
|
||||
return result
|
||||
} catch (backendError) {
|
||||
console.error('❌ Backend project creation failed:', backendError)
|
||||
|
||||
// Enhanced error handling
|
||||
let errorMessage = 'Unknown deployment error'
|
||||
let errorDetails = {
|
||||
message: 'Unknown error',
|
||||
stack: undefined as string | undefined,
|
||||
name: 'UnknownError'
|
||||
}
|
||||
|
||||
if (backendError instanceof Error) {
|
||||
errorMessage = backendError.message
|
||||
errorDetails = {
|
||||
message: backendError.message,
|
||||
stack: backendError.stack,
|
||||
name: backendError.name
|
||||
}
|
||||
} else if (typeof backendError === 'object' && backendError !== null) {
|
||||
const errorObj = backendError as any
|
||||
errorMessage = errorObj.message || JSON.stringify(backendError)
|
||||
errorDetails = {
|
||||
message: errorObj.message || errorMessage,
|
||||
stack: errorObj.stack,
|
||||
name: errorObj.name || 'BackendError'
|
||||
}
|
||||
}
|
||||
|
||||
console.error('❌ Error details:', errorDetails)
|
||||
|
||||
// Provide more specific error messages
|
||||
if (
|
||||
errorMessage.includes('Cannot return null for non-nullable field')
|
||||
) {
|
||||
errorMessage = `Backend validation error: A required field is missing or invalid.
|
||||
|
||||
Possible issues:
|
||||
1. Invalid template ID: "${config.template.id}"
|
||||
2. Repository format issue: "${repoResult.repositoryUrl}"
|
||||
3. Missing deployer configuration
|
||||
4. Organization "${config.organizationSlug}" not found
|
||||
|
||||
Repository was created successfully: ${repoResult.repositoryUrl}
|
||||
You may need to check the backend configuration or contact support.`
|
||||
}
|
||||
|
||||
throw new Error(`Backend deployment setup failed: ${errorMessage}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Template deployment failed:', error)
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Unknown error'
|
||||
setError(errorMessage)
|
||||
toast.error(`Template deployment failed: ${errorMessage}`)
|
||||
throw error
|
||||
} finally {
|
||||
setIsDeploying(false)
|
||||
}
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
setDeploymentResult(null)
|
||||
setError(null)
|
||||
setIsDeploying(false)
|
||||
}
|
||||
|
||||
return {
|
||||
deployTemplate,
|
||||
isDeploying: isDeploying || directGitHub.isLoading,
|
||||
deploymentResult,
|
||||
error,
|
||||
reset
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
'use client'
|
||||
|
||||
import { getGitHubToken } from '@/actions/github'
|
||||
import { useAuth } from '@clerk/nextjs'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
interface UseGitHubTokenReturn {
|
||||
token: string | null
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
refreshToken: () => Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* A hook to get the current user's GitHub OAuth token from Clerk
|
||||
* This ensures each user gets their own token for GitHub API calls
|
||||
*/
|
||||
export function useGitHubToken(): UseGitHubTokenReturn {
|
||||
const [token, setToken] = useState<string | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const { isLoaded: isAuthLoaded, userId } = useAuth()
|
||||
|
||||
const fetchToken = async () => {
|
||||
if (!userId) {
|
||||
setError('User not authenticated')
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
const userToken = await getGitHubToken()
|
||||
|
||||
if (!userToken) {
|
||||
setError(
|
||||
'GitHub account not connected. Please connect your GitHub account.'
|
||||
)
|
||||
setToken(null)
|
||||
} else {
|
||||
console.log('✅ Successfully retrieved user-specific GitHub token')
|
||||
setToken(userToken)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('❌ Error getting GitHub token:', err)
|
||||
setError(
|
||||
err instanceof Error ? err.message : 'Failed to get GitHub token'
|
||||
)
|
||||
setToken(null)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthLoaded) {
|
||||
fetchToken()
|
||||
}
|
||||
}, [isAuthLoaded, userId])
|
||||
|
||||
const refreshToken = async () => {
|
||||
await fetchToken()
|
||||
}
|
||||
|
||||
return { token, isLoading, error, refreshToken }
|
||||
}
|
||||
@@ -1,138 +1,127 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useAuth, useUser } from "@clerk/nextjs";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import { useOctokit } from '@/context/OctokitContext'
|
||||
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);
|
||||
|
||||
// Get auth data from Clerk
|
||||
const { isLoaded: isAuthLoaded } = useAuth();
|
||||
const { isLoaded: isUserLoaded, user } = useUser();
|
||||
const [repoData, setRepoData] = useState<any>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Initialize Octokit with the appropriate token
|
||||
useEffect(() => {
|
||||
async function initializeOctokit() {
|
||||
let token = null;
|
||||
|
||||
// Try to get GitHub token from Clerk
|
||||
if (user) {
|
||||
try {
|
||||
// Check if user has connected GitHub account
|
||||
const githubAccount = user.externalAccounts.find(
|
||||
account => account.provider === 'github'
|
||||
);
|
||||
|
||||
if (githubAccount) {
|
||||
// Try to get GitHub OAuth token from Clerk
|
||||
try {
|
||||
// token = await user.getToken({ template: 'github' });
|
||||
console.log('Using GitHub token from Clerk');
|
||||
} catch (err) {
|
||||
console.error('Error getting GitHub token from Clerk:', err);
|
||||
}
|
||||
}
|
||||
} catch (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) {
|
||||
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]);
|
||||
// Use the centralized Octokit context instead of creating our own
|
||||
const { octokit, isAuth } = useOctokit()
|
||||
|
||||
// Fetch repo data when Octokit is available
|
||||
// Debug the context state
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
console.log('🔍 useRepoData: Context state changed:', {
|
||||
hasOctokit: !!octokit,
|
||||
isAuth,
|
||||
repoId
|
||||
})
|
||||
}, [octokit, isAuth, repoId])
|
||||
|
||||
// Fetch repo data when Octokit is available and authenticated
|
||||
useEffect(() => {
|
||||
let isMounted = true
|
||||
|
||||
async function fetchRepoData() {
|
||||
if (!octokit) {
|
||||
return;
|
||||
// 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
|
||||
}
|
||||
|
||||
|
||||
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 {
|
||||
// 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 (!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: any) => 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)
|
||||
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) {
|
||||
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 && isAuth) {
|
||||
fetchRepoData()
|
||||
} else if (!isAuth) {
|
||||
// Handle case where we're not authenticated yet
|
||||
console.log('🔍 useRepoData: Waiting for authentication...')
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
}
|
||||
|
||||
return () => {
|
||||
isMounted = false
|
||||
}
|
||||
}, [repoId, octokit, isAuth])
|
||||
|
||||
return { repoData, isLoading, error }
|
||||
}
|
||||
|
||||
@@ -44,10 +44,10 @@ Run these steps in the `apps/deployer/` directory:
|
||||
NEXT_PUBLIC_WALLET_IFRAME_URL: https://wallet.laconic.com
|
||||
NEXT_PUBLIC_LACONICD_CHAIN_ID: laconic-mainnet
|
||||
NEXT_PUBLIC_API_URL: https://deploy-backend.apps.vaasl.io
|
||||
NEXT_PUBLIC_GITHUB_FALLBACK_TOKEN: your_github_token
|
||||
NEXT_PUBLIC_GITHUB_BACKEND_CLIENT_ID: Ov23li1hxnCcEggrbwED
|
||||
```
|
||||
|
||||
NOTE: Replace `your_clerk_key`, `your_clerk_secret` and `your_github_token` with actual values
|
||||
NOTE: Replace `your_clerk_key` and `your_clerk_secret` with actual values (<https://clerk.com/docs/quickstarts/nextjs-pages-router#set-your-clerk-api-keys>)
|
||||
|
||||
- Run script to deploy app to `deploy-staging.laconic.co`
|
||||
|
||||
|
||||
@@ -117,6 +117,7 @@ else
|
||||
echo "Payment amount is null; skipping payment."
|
||||
fi
|
||||
|
||||
# TODO: Update dns when transitioning from staging to production deployment (deploy.laconic.com)
|
||||
# Generate application-deployment-request.yml
|
||||
cat >./records/application-deployment-request.yml <<EOF
|
||||
record:
|
||||
@@ -133,7 +134,7 @@ record:
|
||||
NEXT_PUBLIC_API_URL: https://deploy-backend.apps.vaasl.io
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY:
|
||||
CLERK_SECRET_KEY:
|
||||
NEXT_PUBLIC_GITHUB_FALLBACK_TOKEN: your_github_token
|
||||
NEXT_PUBLIC_GITHUB_BACKEND_CLIENT_ID: Ov23li1hxnCcEggrbwED
|
||||
meta:
|
||||
note: Added @ $CURRENT_DATE_TIME
|
||||
repository: "$REPO_URL"
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ CLERK_SECRET_KEY=CERC_RUNTIME_ENV_CLERK_SECRET_KEY
|
||||
NEXT_PUBLIC_WALLET_IFRAME_URL=CERC_RUNTIME_ENV_NEXT_PUBLIC_WALLET_IFRAME_URL
|
||||
NEXT_PUBLIC_LACONICD_CHAIN_ID=CERC_RUNTIME_ENV_NEXT_PUBLIC_LACONICD_CHAIN_ID
|
||||
NEXT_PUBLIC_API_URL=CERC_RUNTIME_ENV_NEXT_PUBLIC_API_URL
|
||||
NEXT_PUBLIC_GITHUB_FALLBACK_TOKEN=CERC_RUNTIME_ENV_NEXT_PUBLIC_GITHUB_FALLBACK_TOKEN
|
||||
NEXT_PUBLIC_GITHUB_BACKEND_CLIENT_ID=CERC_RUNTIME_ENV_NEXT_PUBLIC_GITHUB_BACKEND_CLIENT_ID
|
||||
EOF
|
||||
|
||||
pnpm install || exit 1
|
||||
|
||||
@@ -6,13 +6,13 @@ Ensure you have the following installed:
|
||||
- [Node.js](https://nodejs.org/) (v18+)
|
||||
- [pnpm](https://pnpm.io/) (v8+)
|
||||
- [Git](https://git-scm.com/)
|
||||
- [ngrok](https://ngrok.com/docs/getting-started/#2-install-the-ngrok-agent-cli)
|
||||
|
||||
## Project Structure
|
||||
|
||||
This monorepo contains several packages:
|
||||
|
||||
- `apps/deploy-fe`: Frontend Next.js application
|
||||
- `apps/backend`: Express.js backend API
|
||||
- `apps/deployer`: Deployment service
|
||||
- `services/gql-client`: GraphQL client library
|
||||
- `services/ui`: Shared UI components
|
||||
@@ -20,48 +20,232 @@ This monorepo contains several packages:
|
||||
|
||||
## Getting Started
|
||||
|
||||
### 1. Clone the repository
|
||||
The following steps are for running the deploy app locally (using [laconicd mainnet chain](https://laconicd-mainnet-1.laconic.com/status)):
|
||||
|
||||
```bash
|
||||
git clone https://github.com/yourusername/qwrk-laconic-core-develop.git
|
||||
cd qwrk-laconic-core-develop
|
||||
```
|
||||
- Clone laconic-wallet-web repo
|
||||
```bash
|
||||
git clone https://git.vdb.to/LaconicNetwork/laconic-wallet-web.git
|
||||
cd laconic-wallet-web
|
||||
```
|
||||
|
||||
### 2. Install dependencies
|
||||
- Create .env
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
- Update values in `.env`
|
||||
```
|
||||
# Not required since WalletConnect is not used in iframe integration
|
||||
REACT_APP_WALLET_CONNECT_PROJECT_ID=
|
||||
|
||||
### 3. Configure environment variables
|
||||
REACT_APP_DEFAULT_GAS_PRICE=0.025
|
||||
|
||||
# Reference: https://github.com/cosmos/cosmos-sdk/issues/16020
|
||||
REACT_APP_GAS_ADJUSTMENT=2
|
||||
REACT_APP_LACONICD_RPC_URL=https://laconicd-mainnet-1.laconic.com
|
||||
|
||||
Create a `.env.local` file in the `apps/deploy-fe` directory:
|
||||
# URL of Deploy app frontend app that will run locally
|
||||
REACT_APP_ALLOWED_URLS=http://localhost:3000
|
||||
```
|
||||
|
||||
```
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_key
|
||||
CLERK_SECRET_KEY=your_clerk_secret
|
||||
NEXT_PUBLIC_WALLET_IFRAME_URL=http://localhost:4000
|
||||
NEXT_PUBLIC_LACONICD_CHAIN_ID=laconic-mainnet
|
||||
NEXT_PUBLIC_API_URL=http://localhost:8000
|
||||
NEXT_PUBLIC_GITHUB_FALLBACK_TOKEN=your_github_token
|
||||
```
|
||||
- Install dependencies
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
|
||||
Create a `local.toml` file in `apps/backend/environments/` (based on the example file).
|
||||
- Build app
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
|
||||
### 4. Start the development servers
|
||||
- Set env values in build
|
||||
```bash
|
||||
yarn set-env
|
||||
```
|
||||
|
||||
#### Frontend:
|
||||
```bash
|
||||
cd apps/deploy-fe
|
||||
pnpm dev
|
||||
```
|
||||
- Serve the build
|
||||
```
|
||||
# Install package serve globally
|
||||
npm install -g serve
|
||||
|
||||
### 5. Connect Your Wallet
|
||||
# Serve the wallet build
|
||||
serve -s -l 4000 ./build
|
||||
```
|
||||
|
||||
The application requires a Laconic wallet for certain operations. You need to set up:
|
||||
Wallet will run at http://localhost:4000
|
||||
|
||||
- In a new terminal, clone repo for backend
|
||||
```bash
|
||||
git clone https://git.vdb.to/cerc-io/snowballtools-base.git
|
||||
cd snowballtools-base
|
||||
```
|
||||
|
||||
- Install deps and build
|
||||
```bash
|
||||
yarn && yarn build --ignore frontend
|
||||
```
|
||||
|
||||
- Create `packages/backend/environments/local.toml`
|
||||
```bash
|
||||
cp packages/backend/environments/local.toml.example packages/backend/environments/local.toml
|
||||
```
|
||||
|
||||
- Update values in `packages/backend/environments/local.toml`
|
||||
```toml
|
||||
[server]
|
||||
host = "127.0.0.1"
|
||||
port = 8000
|
||||
gqlPath = "/graphql"
|
||||
[server.session]
|
||||
# Can be set to any random string
|
||||
secret = "RpwqcvFkLZ"
|
||||
# Frontend webapp URL origin
|
||||
appOriginUrl = "http://localhost:3000"
|
||||
# Set to true if server running behind proxy
|
||||
trustProxy = false
|
||||
# Backend URL hostname
|
||||
domain = "localhost"
|
||||
|
||||
[database]
|
||||
dbPath = "db/snowball"
|
||||
|
||||
[gitHub]
|
||||
webhookUrl = ""
|
||||
[gitHub.oAuth]
|
||||
clientId = "Ov23lihCWQvOORNxtebD"
|
||||
clientSecret = "139de35bdd610628289adcd99bb2c7c962a11c99"
|
||||
|
||||
[registryConfig]
|
||||
fetchDeploymentRecordDelay = 5000
|
||||
checkAuctionStatusDelay = 5000
|
||||
restEndpoint = "https://laconicd-mainnet-1.laconic.com"
|
||||
gqlEndpoint = "https://laconicd-mainnet-1.laconic.com/api"
|
||||
chainId = "laconic-mainnet"
|
||||
# Set private key of account laconic13maulvmjxnyx3g855vk0lsv5aptf3rpxskynef
|
||||
# This account owns the bond and authority configured below
|
||||
privateKey = ""
|
||||
bondId = "230cfedda15e78edc8986dfcb870e1b618f65c56e38d2735476d2a8cb3f25e38"
|
||||
authority = "laconic"
|
||||
[registryConfig.fee]
|
||||
gasPrice = "0.001alnt"
|
||||
|
||||
[auction]
|
||||
commitFee = "1000"
|
||||
commitsDuration = "60s"
|
||||
revealFee = "1000"
|
||||
revealsDuration = "60s"
|
||||
denom = "alnt"
|
||||
```
|
||||
|
||||
- Run ngrok
|
||||
```bash
|
||||
ngrok http 8000
|
||||
```
|
||||
|
||||
- Set ngrok URL to `gitHub.webhookUrl` in config `packages/backend/environments/local.toml`
|
||||
- Example
|
||||
```toml
|
||||
...
|
||||
[gitHub]
|
||||
webhookUrl = "https://<ngrok-url>.ngrok-free.app"
|
||||
...
|
||||
```
|
||||
|
||||
- Run backend server
|
||||
```bash
|
||||
cd packages/backend
|
||||
yarn start
|
||||
```
|
||||
|
||||
- In a new terminal, clone repo for frontend (laconic-deployer-frontend)
|
||||
```bash
|
||||
git clone https://git.vdb.to/NasSharaf/laconic-deployer-frontend.git
|
||||
cd laconic-deployer-frontend
|
||||
```
|
||||
|
||||
- Install dependencies
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
- Build dependecies for frontend app
|
||||
```bash
|
||||
pnpm build:fe-compile
|
||||
```
|
||||
|
||||
- Create a `.env.local` file in the `apps/deploy-fe` directory to configure environment variables:
|
||||
```
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_key
|
||||
CLERK_SECRET_KEY=your_clerk_secret
|
||||
NEXT_PUBLIC_WALLET_IFRAME_URL=http://localhost:4000
|
||||
NEXT_PUBLIC_LACONICD_CHAIN_ID=laconic-mainnet
|
||||
NEXT_PUBLIC_API_URL=http://localhost:8000
|
||||
NEXT_PUBLIC_GITHUB_BACKEND_CLIENT_ID=Ov23lihCWQvOORNxtebD
|
||||
```
|
||||
Replace `your_clerk_key` and `your_clerk_secret` with actual values (<https://clerk.com/docs/quickstarts/nextjs-pages-router#set-your-clerk-api-keys>)
|
||||
|
||||
- Start the development server
|
||||
```bash
|
||||
cd apps/deploy-fe
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
- The deploy frontend app will be now available at <http://localhost:3000>
|
||||
|
||||
### Fund wallet account
|
||||
|
||||
- Change directory to deployer package
|
||||
```bash
|
||||
cd apps/deployer/
|
||||
```
|
||||
|
||||
- Setup config for laconic-registry-cli in `packages/deployer/config.yml`
|
||||
```yaml
|
||||
services:
|
||||
registry:
|
||||
rpcEndpoint: https://laconicd-mainnet-1.laconic.com
|
||||
gqlEndpoint: https://laconicd-mainnet-1.laconic.com/api
|
||||
# Set to private key of an account with funds
|
||||
# Private key of account laconic13maulvmjxnyx3g855vk0lsv5aptf3rpxskynef set in deployer backend can be used
|
||||
userKey:
|
||||
chainId: laconic-testnet-2
|
||||
gasPrice: 0.001alnt
|
||||
```
|
||||
|
||||
- Get wallet account address at <http://localhost:4000> for `laconicd mainnet` network
|
||||
|
||||
- Run command to send tokens
|
||||
```bash
|
||||
pnpm laconic registry tokens send --address <ACCOUNT_ADDRESS_FROM_WALLET> --type alnt --quantity 12960
|
||||
```
|
||||
|
||||
- To check balance visit <https://explorer.laconic.com/laconic-mainnet/account/ACCOUNT_ADDRESS_FROM_WALLET>
|
||||
|
||||
### Misc
|
||||
|
||||
- To check for deployments in vaasl : <https://webapp-deployer-ui.apps.vaasl.io/>
|
||||
|
||||
- If deployment fails due to low bond balance
|
||||
- Change directory to deployer package
|
||||
```bash
|
||||
cd apps/deployer/
|
||||
```
|
||||
|
||||
- Check balances
|
||||
```bash
|
||||
# Account balance
|
||||
yarn laconic registry account get --address laconic13maulvmjxnyx3g855vk0lsv5aptf3rpxskynef
|
||||
|
||||
# Bond balance
|
||||
yarn laconic registry bond get --id 230cfedda15e78edc8986dfcb870e1b618f65c56e38d2735476d2a8cb3f25e38
|
||||
```
|
||||
|
||||
- Command to refill bond
|
||||
|
||||
```bash
|
||||
yarn laconic registry bond refill --id 230cfedda15e78edc8986dfcb870e1b618f65c56e38d2735476d2a8cb3f25e38 --type alnt --quantity 10000000
|
||||
```
|
||||
|
||||
1. A running wallet instance (follow the setup in the laconic-wallet-web repository)
|
||||
2. Configure the `NEXT_PUBLIC_WALLET_IFRAME_URL` to point to your wallet instance
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -96,7 +280,7 @@ Make sure you have:
|
||||
|
||||
## Test deployment
|
||||
|
||||
- Follow the dev install steps for stack-orchestrator from <https://git.vdb.to/cerc-io/stack-orchestrator/src/branch/main/docs/CONTRIBUTING.md#install>
|
||||
- Install stack-orchestrator from <https://git.vdb.to/cerc-io/stack-orchestrator#install>
|
||||
|
||||
- Build the container for app
|
||||
|
||||
|
||||
Reference in New Issue
Block a user