Connected to backend and fixed up UI changes, created a test-connection page
This commit is contained in:
parent
16bb8acc7e
commit
4512ef1d8a
@ -3,14 +3,15 @@
|
|||||||
import { useParams } from 'next/navigation';
|
import { useParams } from 'next/navigation';
|
||||||
import { PageWrapper } from '@/components/foundation';
|
import { PageWrapper } from '@/components/foundation';
|
||||||
import { DeploymentDetailsCard } from '@/components/projects/project/deployments/DeploymentDetailsCard';
|
import { DeploymentDetailsCard } from '@/components/projects/project/deployments/DeploymentDetailsCard';
|
||||||
import { FilterForm } from '@/components/projects/project/deployments/FilterForm';
|
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||||
import { IconButton } from '@workspace/ui/components/button';
|
import { Button } from '@workspace/ui/components/button';
|
||||||
import { Rocket } from 'lucide-react';
|
import { Square, Search, Calendar, ChevronDown } from 'lucide-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useRepoData } from '@/hooks/useRepoData';
|
import { useRepoData } from '@/hooks/useRepoData';
|
||||||
import type { Deployment, Domain } from '@/types';
|
import type { Deployment, Domain } from '@/types';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@workspace/ui/components/dialog';
|
||||||
|
import { Input } from '@workspace/ui/components/input';
|
||||||
|
|
||||||
export default function DeploymentsPage() {
|
export default function DeploymentsPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -26,6 +27,11 @@ export default function DeploymentsPage() {
|
|||||||
const [deployments, setDeployments] = useState<Deployment[]>([]);
|
const [deployments, setDeployments] = useState<Deployment[]>([]);
|
||||||
const [filteredDeployments, setFilteredDeployments] = useState<Deployment[]>([]);
|
const [filteredDeployments, setFilteredDeployments] = useState<Deployment[]>([]);
|
||||||
const [prodBranchDomains, setProdBranchDomains] = useState<Domain[]>([]);
|
const [prodBranchDomains, setProdBranchDomains] = useState<Domain[]>([]);
|
||||||
|
|
||||||
|
// State for deployment logs modal
|
||||||
|
const [isLogsOpen, setIsLogsOpen] = useState(false);
|
||||||
|
const [selectedDeploymentId, setSelectedDeploymentId] = useState<string | null>(null);
|
||||||
|
const [deploymentLogs, setDeploymentLogs] = useState<string>('');
|
||||||
|
|
||||||
// Create a default deployment
|
// Create a default deployment
|
||||||
const defaultDeployment: Deployment = {
|
const defaultDeployment: Deployment = {
|
||||||
@ -56,11 +62,17 @@ export default function DeploymentsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize with mock data
|
// Initialize with empty data for testing the empty state
|
||||||
|
// Comment this out to see the mock deployments
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const mockDeployments = [defaultDeployment, secondDeployment];
|
// For testing the empty state
|
||||||
setDeployments(mockDeployments);
|
setDeployments([]);
|
||||||
setFilteredDeployments(mockDeployments);
|
setFilteredDeployments([]);
|
||||||
|
|
||||||
|
// Uncomment to see mock deployments
|
||||||
|
// const mockDeployments = [defaultDeployment, secondDeployment];
|
||||||
|
// setDeployments(mockDeployments);
|
||||||
|
// setFilteredDeployments(mockDeployments);
|
||||||
|
|
||||||
// Mock domains
|
// Mock domains
|
||||||
const mockDomains: Domain[] = [
|
const mockDomains: Domain[] = [
|
||||||
@ -102,6 +114,31 @@ export default function DeploymentsPage() {
|
|||||||
setFilteredDeployments(deployments);
|
setFilteredDeployments(deployments);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// View logs handler
|
||||||
|
const handleViewLogs = (deploymentId: string) => {
|
||||||
|
setSelectedDeploymentId(deploymentId);
|
||||||
|
|
||||||
|
// Mock logs data
|
||||||
|
const mockLogs = `[2025-02-12 10:03:12] INFO Starting deployment process for service: api-gateway
|
||||||
|
[2025-02-12 10:03:14] INFO Fetching latest commit from main branch (commit: a1b2c3d)
|
||||||
|
[2025-02-12 10:03:15] INFO Building Docker image: registry.company.com/api-gateway:latest
|
||||||
|
[2025-02-12 10:03:26] INFO Running security scan on built image
|
||||||
|
[2025-02-12 10:03:27] WARNING Medium severity vulnerability detected in package 'openssl'
|
||||||
|
[2025-02-12 10:03:30] INFO Pushing image to container registry
|
||||||
|
[2025-02-12 10:03:35] INFO Updating Kubernetes deployment
|
||||||
|
[2025-02-12 10:03:40] INFO Scaling down old pods
|
||||||
|
[2025-02-12 10:03:42] INFO Scaling up new pods
|
||||||
|
[2025-02-12 10:03:50] INFO Running health checks on new pods
|
||||||
|
[2025-02-12 10:03:52] ERROR Pod 'api-gateway-7df9bbb500-tx2k4' failed readiness probe (502 Bad Gateway)
|
||||||
|
[2025-02-12 10:03:55] INFO Retrying deployment with previous stable image
|
||||||
|
[2025-02-12 10:04:03] INFO Rolling back to registry.company.com/api-gateway:previous
|
||||||
|
[2025-02-12 10:04:10] INFO Deployment rolled back successfully
|
||||||
|
[2025-02-12 10:04:11] ERROR Deployment failed, please review logs and fix errors`;
|
||||||
|
|
||||||
|
setDeploymentLogs(mockLogs);
|
||||||
|
setIsLogsOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
const project = {
|
const project = {
|
||||||
id: id,
|
id: id,
|
||||||
prodBranch: 'main',
|
prodBranch: 'main',
|
||||||
@ -110,6 +147,8 @@ export default function DeploymentsPage() {
|
|||||||
|
|
||||||
const currentDeployment = deployments.find(deployment => deployment.isCurrent) || defaultDeployment;
|
const currentDeployment = deployments.find(deployment => deployment.isCurrent) || defaultDeployment;
|
||||||
|
|
||||||
|
const hasDeployments = deployments.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper
|
<PageWrapper
|
||||||
header={{
|
header={{
|
||||||
@ -145,41 +184,108 @@ export default function DeploymentsPage() {
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<FilterForm />
|
{/* Filter Controls - Always visible but disabled when no deployments */}
|
||||||
|
<div className="flex flex-wrap gap-4 mb-4">
|
||||||
|
{/* Search box */}
|
||||||
|
<div className="relative flex-grow max-w-md">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<Search className="h-4 w-4 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search branches"
|
||||||
|
className="pl-10"
|
||||||
|
disabled={!hasDeployments}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Date selector */}
|
||||||
|
<div className="relative">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="flex items-center opacity-60"
|
||||||
|
disabled={!hasDeployments}
|
||||||
|
>
|
||||||
|
<Calendar className="h-4 w-4 mr-2" />
|
||||||
|
<span>Select a date</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status dropdown */}
|
||||||
|
<div className="relative">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="flex items-center opacity-60"
|
||||||
|
disabled={!hasDeployments}
|
||||||
|
>
|
||||||
|
<span>All Status</span>
|
||||||
|
<ChevronDown className="h-4 w-4 ml-2" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="h-full mt-4">
|
<div className="h-full mt-4">
|
||||||
{filteredDeployments.length > 0 ? (
|
{filteredDeployments.length > 0 ? (
|
||||||
filteredDeployments.map((deployment) => (
|
filteredDeployments.map((deployment) => (
|
||||||
<DeploymentDetailsCard
|
<div key={deployment.id} className="mb-4">
|
||||||
key={deployment.id}
|
<DeploymentDetailsCard
|
||||||
deployment={deployment}
|
deployment={deployment}
|
||||||
currentDeployment={currentDeployment}
|
currentDeployment={currentDeployment}
|
||||||
project={project}
|
project={project}
|
||||||
prodBranchDomains={prodBranchDomains}
|
prodBranchDomains={prodBranchDomains}
|
||||||
/>
|
/>
|
||||||
|
<div className="mt-2 flex justify-end">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleViewLogs(deployment.id)}
|
||||||
|
>
|
||||||
|
View logs
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="h-96 bg-base-bg-alternate dark:bg-overlay3 rounded-xl flex flex-col items-center justify-center gap-5 text-center">
|
// Updated empty state to match screenshot
|
||||||
<div className="space-y-1">
|
<div className="h-96 border border-gray-800 rounded-lg flex flex-col items-center justify-center gap-5 text-center">
|
||||||
<p className="font-medium tracking-[-0.011em] text-elements-high-em dark:text-foreground">
|
<div className="mb-6">
|
||||||
No deployments found
|
<Square size={64} className="stroke-current" />
|
||||||
</p>
|
|
||||||
<p className="text-sm tracking-[-0.006em] text-elements-mid-em dark:text-foreground-secondary">
|
|
||||||
Please change your search query or filters.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<IconButton
|
<h2 className="text-xl font-semibold mb-2">You have no deployments</h2>
|
||||||
|
<p className="text-gray-400 text-center max-w-md mb-6">
|
||||||
|
Please change your search query or filters.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
leftIcon={<Rocket className="w-4 h-4" />}
|
|
||||||
onClick={handleResetFilters}
|
onClick={handleResetFilters}
|
||||||
>
|
>
|
||||||
RESET FILTERS
|
Reset filters
|
||||||
</IconButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Deployment Logs Modal */}
|
||||||
|
<Dialog open={isLogsOpen} onOpenChange={setIsLogsOpen}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[80vh] h-[80vh] flex flex-col">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Deployment Logs</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex-1 overflow-auto mt-4">
|
||||||
|
<pre className="bg-black text-green-400 p-4 rounded text-sm font-mono whitespace-pre overflow-x-auto">
|
||||||
|
{deploymentLogs}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end mt-4">
|
||||||
|
<Button variant="secondary" onClick={() => setIsLogsOpen(false)}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useRouter, useParams } from "next/navigation";
|
|
||||||
import { LoadingOverlay } from "@/components/foundation/loading/loading-overlay";
|
import { LoadingOverlay } from "@/components/foundation/loading/loading-overlay";
|
||||||
|
|
||||||
interface SwitchProps {
|
interface SwitchProps {
|
||||||
@ -26,12 +25,12 @@ function Switch({ id, checked, onChange, disabled = false }: SwitchProps) {
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={`relative w-11 h-6 bg-gray-800 rounded-full transition-colors
|
className={`relative w-11 h-6 bg-muted rounded-full transition-colors
|
||||||
${checked ? 'bg-blue-600' : 'bg-gray-700'}`}
|
${checked ? 'bg-primary' : 'bg-muted'}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`absolute w-4 h-4 bg-white rounded-full transition-transform transform
|
className={`absolute w-4 h-4 bg-background rounded-full transition-transform transform
|
||||||
${checked ? 'translate-x-6' : 'translate-x-1'} top-1`}
|
${checked ? 'translate-x-6' : 'translate-x-1'} top-1 border border-border`}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
@ -39,9 +38,6 @@ function Switch({ id, checked, onChange, disabled = false }: SwitchProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function GitPage() {
|
export default function GitPage() {
|
||||||
const params = useParams();
|
|
||||||
const { provider, id } = params;
|
|
||||||
|
|
||||||
const [pullRequestComments, setPullRequestComments] = useState(true);
|
const [pullRequestComments, setPullRequestComments] = useState(true);
|
||||||
const [commitComments, setCommitComments] = useState(false);
|
const [commitComments, setCommitComments] = useState(false);
|
||||||
const [productionBranch, setProductionBranch] = useState("main");
|
const [productionBranch, setProductionBranch] = useState("main");
|
||||||
@ -88,8 +84,8 @@ export default function GitPage() {
|
|||||||
{(isSavingBranch || isSavingWebhook) && <LoadingOverlay />}
|
{(isSavingBranch || isSavingWebhook) && <LoadingOverlay />}
|
||||||
|
|
||||||
<div className="space-y-8 w-full">
|
<div className="space-y-8 w-full">
|
||||||
<div className="rounded-lg border border-gray-800 p-6 bg-black">
|
<div className="rounded-lg border border-border p-6 bg-card">
|
||||||
<h2 className="text-xl font-semibold mb-4">Git repository</h2>
|
<h2 className="text-xl font-semibold mb-4 text-foreground">Git repository</h2>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
@ -100,11 +96,11 @@ export default function GitPage() {
|
|||||||
checked={pullRequestComments}
|
checked={pullRequestComments}
|
||||||
onChange={setPullRequestComments}
|
onChange={setPullRequestComments}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="pull-request-comments" className="text-sm font-medium">
|
<label htmlFor="pull-request-comments" className="text-sm font-medium text-foreground">
|
||||||
Pull request comments
|
Pull request comments
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-400 mt-1 ml-14">
|
<p className="text-sm text-muted-foreground mt-1 ml-14">
|
||||||
Laconic will comment on pull requests opened against this project.
|
Laconic will comment on pull requests opened against this project.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -118,11 +114,11 @@ export default function GitPage() {
|
|||||||
checked={commitComments}
|
checked={commitComments}
|
||||||
onChange={setCommitComments}
|
onChange={setCommitComments}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="commit-comments" className="text-sm font-medium">
|
<label htmlFor="commit-comments" className="text-sm font-medium text-foreground">
|
||||||
Commit comments
|
Commit comments
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-400 mt-1 ml-14">
|
<p className="text-sm text-muted-foreground mt-1 ml-14">
|
||||||
Laconic will comment on commits deployed to production.
|
Laconic will comment on commits deployed to production.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -130,47 +126,47 @@ export default function GitPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-lg border border-gray-800 p-6 bg-black">
|
<div className="rounded-lg border border-border p-6 bg-card">
|
||||||
<h2 className="text-xl font-semibold mb-4">Production branch</h2>
|
<h2 className="text-xl font-semibold mb-4 text-foreground">Production branch</h2>
|
||||||
|
|
||||||
<p className="text-sm text-gray-400 mb-4">
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
By default, each commit pushed to the main branch initiates a production deployment. You can opt for a
|
By default, each commit pushed to the main branch initiates a production deployment. You can opt for a
|
||||||
different branch for deployment in the settings.
|
different branch for deployment in the settings.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="branch-name" className="block text-sm font-medium mb-1">
|
<label htmlFor="branch-name" className="block text-sm font-medium mb-1 text-foreground">
|
||||||
Branch name
|
Branch name
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="branch-name"
|
id="branch-name"
|
||||||
value={productionBranch}
|
value={productionBranch}
|
||||||
onChange={(e) => setProductionBranch(e.target.value)}
|
onChange={(e) => setProductionBranch(e.target.value)}
|
||||||
className="w-full px-3 py-2 rounded-md bg-gray-900 border border-gray-700 text-white"
|
className="w-full px-3 py-2 rounded-md bg-background border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="px-4 py-2 border border-gray-600 rounded-md hover:bg-gray-800 transition-colors"
|
className="px-4 py-2 border border-border rounded-md hover:bg-accent hover:text-accent-foreground transition-colors bg-background text-foreground"
|
||||||
onClick={handleSaveBranch}
|
onClick={handleSaveBranch}
|
||||||
disabled={isSavingBranch}
|
disabled={isSavingBranch}
|
||||||
>
|
>
|
||||||
Save
|
{isSavingBranch ? "Saving..." : "Save"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-lg border border-gray-800 p-6 bg-black">
|
<div className="rounded-lg border border-border p-6 bg-card">
|
||||||
<h2 className="text-xl font-semibold mb-4">Deploy webhooks</h2>
|
<h2 className="text-xl font-semibold mb-4 text-foreground">Deploy webhooks</h2>
|
||||||
|
|
||||||
<p className="text-sm text-gray-400 mb-4">
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
Webhooks configured to trigger when there is a change in a project's build or deployment status.
|
Webhooks configured to trigger when there is a change in a project's build or deployment status.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="webhook-url" className="block text-sm font-medium mb-1">
|
<label htmlFor="webhook-url" className="block text-sm font-medium mb-1 text-foreground">
|
||||||
Webhook URL
|
Webhook URL
|
||||||
</label>
|
</label>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
@ -179,14 +175,14 @@ export default function GitPage() {
|
|||||||
value={webhookUrl}
|
value={webhookUrl}
|
||||||
onChange={(e) => setWebhookUrl(e.target.value)}
|
onChange={(e) => setWebhookUrl(e.target.value)}
|
||||||
placeholder="https://"
|
placeholder="https://"
|
||||||
className="flex-1 px-3 py-2 rounded-l-md bg-gray-900 border border-gray-700 text-white"
|
className="flex-1 px-3 py-2 rounded-l-md bg-background border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
className="px-4 py-2 border border-gray-600 border-l-0 rounded-r-md hover:bg-gray-800 transition-colors"
|
className="px-4 py-2 border border-border border-l-0 rounded-r-md hover:bg-accent hover:text-accent-foreground transition-colors bg-background text-foreground"
|
||||||
onClick={handleSaveWebhook}
|
onClick={handleSaveWebhook}
|
||||||
disabled={isSavingWebhook}
|
disabled={isSavingWebhook}
|
||||||
>
|
>
|
||||||
Save
|
{isSavingWebhook ? "Saving..." : "Save"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useRouter, useParams } from "next/navigation";
|
|
||||||
import { LoadingOverlay } from "@/components/foundation/loading/loading-overlay";
|
import { LoadingOverlay } from "@/components/foundation/loading/loading-overlay";
|
||||||
import { PlusIcon, ChevronDownIcon, ChevronUpIcon, PencilIcon, TrashIcon } from "lucide-react";
|
import { PlusIcon, ChevronDownIcon, ChevronUpIcon, PencilIcon, TrashIcon } from "lucide-react";
|
||||||
|
|
||||||
@ -21,16 +20,16 @@ interface EnvGroupProps {
|
|||||||
|
|
||||||
function EnvGroup({ title, isOpen, onToggle, children, varCount }: EnvGroupProps) {
|
function EnvGroup({ title, isOpen, onToggle, children, varCount }: EnvGroupProps) {
|
||||||
return (
|
return (
|
||||||
<div className="border-b border-gray-800 last:border-b-0">
|
<div className="border-b border-border last:border-b-0">
|
||||||
<div
|
<div
|
||||||
className="flex items-center justify-between py-4 cursor-pointer"
|
className="flex items-center justify-between py-4 cursor-pointer"
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<h3 className="text-lg font-medium">{title}</h3>
|
<h3 className="text-lg font-medium text-foreground">{title}</h3>
|
||||||
<span className="text-sm text-gray-400">({varCount})</span>
|
<span className="text-sm text-muted-foreground">({varCount})</span>
|
||||||
</div>
|
</div>
|
||||||
<button className="p-1">
|
<button className="p-1 text-foreground hover:text-accent-foreground">
|
||||||
{isOpen ? <ChevronUpIcon size={18} /> : <ChevronDownIcon size={18} />}
|
{isOpen ? <ChevronUpIcon size={18} /> : <ChevronDownIcon size={18} />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -44,9 +43,6 @@ function EnvGroup({ title, isOpen, onToggle, children, varCount }: EnvGroupProps
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function EnvVarsPage() {
|
export default function EnvVarsPage() {
|
||||||
const params = useParams();
|
|
||||||
const { provider, id } = params;
|
|
||||||
|
|
||||||
const [isAddingVar, setIsAddingVar] = useState(false);
|
const [isAddingVar, setIsAddingVar] = useState(false);
|
||||||
const [newVarKey, setNewVarKey] = useState("");
|
const [newVarKey, setNewVarKey] = useState("");
|
||||||
const [newVarValue, setNewVarValue] = useState("");
|
const [newVarValue, setNewVarValue] = useState("");
|
||||||
@ -175,7 +171,7 @@ export default function EnvVarsPage() {
|
|||||||
return (
|
return (
|
||||||
<div key={index} className="flex items-center space-x-2 mb-2">
|
<div key={index} className="flex items-center space-x-2 mb-2">
|
||||||
<input
|
<input
|
||||||
className="flex-1 px-3 py-2 rounded-md bg-gray-900 border border-gray-700 text-white"
|
className="flex-1 px-3 py-2 rounded-md bg-background border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
value={variable.key}
|
value={variable.key}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const updatedVars = env === 'production'
|
const updatedVars = env === 'production'
|
||||||
@ -191,7 +187,7 @@ export default function EnvVarsPage() {
|
|||||||
placeholder="KEY"
|
placeholder="KEY"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
className="flex-1 px-3 py-2 rounded-md bg-gray-900 border border-gray-700 text-white"
|
className="flex-1 px-3 py-2 rounded-md bg-background border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
value={variable.value}
|
value={variable.value}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const updatedVars = env === 'production'
|
const updatedVars = env === 'production'
|
||||||
@ -207,13 +203,13 @@ export default function EnvVarsPage() {
|
|||||||
placeholder="Value"
|
placeholder="Value"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
className="p-2 hover:bg-gray-800 rounded-md"
|
className="p-2 hover:bg-accent hover:text-accent-foreground rounded-md text-foreground transition-colors"
|
||||||
onClick={() => updateVariable(env, index, variable.key, variable.value)}
|
onClick={() => updateVariable(env, index, variable.key, variable.value)}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="p-2 hover:bg-gray-800 rounded-md"
|
className="p-2 hover:bg-accent hover:text-accent-foreground rounded-md text-foreground transition-colors"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const updatedVars = env === 'production'
|
const updatedVars = env === 'production'
|
||||||
? [...productionVars]
|
? [...productionVars]
|
||||||
@ -234,19 +230,19 @@ export default function EnvVarsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={index} className="flex items-center mb-2">
|
<div key={index} className="flex items-center mb-2">
|
||||||
<div className="flex-1 flex items-center justify-between px-3 py-2 rounded-md bg-gray-900 border border-gray-700 mr-2">
|
<div className="flex-1 flex items-center justify-between px-3 py-2 rounded-md bg-muted border border-border mr-2">
|
||||||
<span>{variable.key}</span>
|
<span className="text-foreground">{variable.key}</span>
|
||||||
<span>{variable.value}</span>
|
<span className="text-foreground">{variable.value}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex space-x-1">
|
<div className="flex space-x-1">
|
||||||
<button
|
<button
|
||||||
className="p-2 hover:bg-gray-800 rounded-md"
|
className="p-2 hover:bg-accent hover:text-accent-foreground rounded-md text-muted-foreground transition-colors"
|
||||||
onClick={() => editVariable(env, index)}
|
onClick={() => editVariable(env, index)}
|
||||||
>
|
>
|
||||||
<PencilIcon size={16} />
|
<PencilIcon size={16} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="p-2 hover:bg-gray-800 rounded-md"
|
className="p-2 hover:bg-accent hover:text-accent-foreground rounded-md text-muted-foreground transition-colors"
|
||||||
onClick={() => removeVariable(env, index)}
|
onClick={() => removeVariable(env, index)}
|
||||||
>
|
>
|
||||||
<TrashIcon size={16} />
|
<TrashIcon size={16} />
|
||||||
@ -261,45 +257,45 @@ export default function EnvVarsPage() {
|
|||||||
{isSaving && <LoadingOverlay />}
|
{isSaving && <LoadingOverlay />}
|
||||||
|
|
||||||
<div className="space-y-6 w-full">
|
<div className="space-y-6 w-full">
|
||||||
<div className="rounded-lg border border-gray-800 p-6 bg-black">
|
<div className="rounded-lg border border-border p-6 bg-card">
|
||||||
<h2 className="text-xl font-semibold mb-4">Environment Variables</h2>
|
<h2 className="text-xl font-semibold mb-4 text-foreground">Environment Variables</h2>
|
||||||
<p className="text-sm text-gray-400 mb-6">
|
<p className="text-sm text-muted-foreground mb-6">
|
||||||
A new deployment is required for your changes to take effect.
|
A new deployment is required for your changes to take effect.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{!isAddingVar ? (
|
{!isAddingVar ? (
|
||||||
<button
|
<button
|
||||||
className="flex items-center space-x-2 px-4 py-2 rounded-md border border-gray-700 bg-gray-900 hover:bg-gray-800 transition-colors"
|
className="flex items-center space-x-2 px-4 py-2 rounded-md border border-border bg-background hover:bg-accent hover:text-accent-foreground transition-colors text-foreground"
|
||||||
onClick={() => setIsAddingVar(true)}
|
onClick={() => setIsAddingVar(true)}
|
||||||
>
|
>
|
||||||
<PlusIcon size={16} />
|
<PlusIcon size={16} />
|
||||||
<span>Create new variable</span>
|
<span>Create new variable</span>
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4 mb-6 border border-gray-800 p-4 rounded-md">
|
<div className="space-y-4 mb-6 border border-border p-4 rounded-md bg-background">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1">Key</label>
|
<label className="block text-sm font-medium mb-1 text-foreground">Key</label>
|
||||||
<input
|
<input
|
||||||
value={newVarKey}
|
value={newVarKey}
|
||||||
onChange={(e) => setNewVarKey(e.target.value)}
|
onChange={(e) => setNewVarKey(e.target.value)}
|
||||||
className="w-full px-3 py-2 rounded-md bg-gray-900 border border-gray-700 text-white"
|
className="w-full px-3 py-2 rounded-md bg-background border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
placeholder="KEY"
|
placeholder="KEY"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1">Value</label>
|
<label className="block text-sm font-medium mb-1 text-foreground">Value</label>
|
||||||
<input
|
<input
|
||||||
value={newVarValue}
|
value={newVarValue}
|
||||||
onChange={(e) => setNewVarValue(e.target.value)}
|
onChange={(e) => setNewVarValue(e.target.value)}
|
||||||
className="w-full px-3 py-2 rounded-md bg-gray-900 border border-gray-700 text-white"
|
className="w-full px-3 py-2 rounded-md bg-background border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
placeholder="Value"
|
placeholder="Value"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-2">Environments</label>
|
<label className="block text-sm font-medium mb-2 text-foreground">Environments</label>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<input
|
<input
|
||||||
@ -307,9 +303,9 @@ export default function EnvVarsPage() {
|
|||||||
id="env-production"
|
id="env-production"
|
||||||
checked={envSelection.production}
|
checked={envSelection.production}
|
||||||
onChange={() => handleEnvSelectionChange('production')}
|
onChange={() => handleEnvSelectionChange('production')}
|
||||||
className="mr-2"
|
className="mr-2 rounded border-border"
|
||||||
/>
|
/>
|
||||||
<label htmlFor="env-production">Production</label>
|
<label htmlFor="env-production" className="text-foreground">Production</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<input
|
<input
|
||||||
@ -317,9 +313,9 @@ export default function EnvVarsPage() {
|
|||||||
id="env-preview"
|
id="env-preview"
|
||||||
checked={envSelection.preview}
|
checked={envSelection.preview}
|
||||||
onChange={() => handleEnvSelectionChange('preview')}
|
onChange={() => handleEnvSelectionChange('preview')}
|
||||||
className="mr-2"
|
className="mr-2 rounded border-border"
|
||||||
/>
|
/>
|
||||||
<label htmlFor="env-preview">Preview</label>
|
<label htmlFor="env-preview" className="text-foreground">Preview</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<input
|
<input
|
||||||
@ -327,22 +323,22 @@ export default function EnvVarsPage() {
|
|||||||
id="env-development"
|
id="env-development"
|
||||||
checked={envSelection.development}
|
checked={envSelection.development}
|
||||||
onChange={() => handleEnvSelectionChange('development')}
|
onChange={() => handleEnvSelectionChange('development')}
|
||||||
className="mr-2"
|
className="mr-2 rounded border-border"
|
||||||
/>
|
/>
|
||||||
<label htmlFor="env-development">Development</label>
|
<label htmlFor="env-development" className="text-foreground">Development</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end space-x-2">
|
<div className="flex justify-end space-x-2">
|
||||||
<button
|
<button
|
||||||
className="px-4 py-2 rounded-md border border-gray-700 hover:bg-gray-800 transition-colors"
|
className="px-4 py-2 rounded-md border border-border hover:bg-accent hover:text-accent-foreground transition-colors bg-background text-foreground"
|
||||||
onClick={cancelAddVariable}
|
onClick={cancelAddVariable}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="px-4 py-2 rounded-md bg-blue-600 hover:bg-blue-700 transition-colors"
|
className="px-4 py-2 rounded-md bg-primary hover:bg-primary/90 transition-colors text-primary-foreground"
|
||||||
onClick={addVariable}
|
onClick={addVariable}
|
||||||
disabled={!newVarKey.trim() || !newVarValue.trim()}
|
disabled={!newVarKey.trim() || !newVarValue.trim()}
|
||||||
>
|
>
|
||||||
@ -364,7 +360,7 @@ export default function EnvVarsPage() {
|
|||||||
{productionVars.map((variable, index) => renderEnvVarRow('production', variable, index))}
|
{productionVars.map((variable, index) => renderEnvVarRow('production', variable, index))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-gray-400">No variables defined</p>
|
<p className="text-sm text-muted-foreground">No variables defined</p>
|
||||||
)}
|
)}
|
||||||
</EnvGroup>
|
</EnvGroup>
|
||||||
|
|
||||||
@ -379,7 +375,7 @@ export default function EnvVarsPage() {
|
|||||||
{previewVars.map((variable, index) => renderEnvVarRow('preview', variable, index))}
|
{previewVars.map((variable, index) => renderEnvVarRow('preview', variable, index))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-gray-400">No variables defined</p>
|
<p className="text-sm text-muted-foreground">No variables defined</p>
|
||||||
)}
|
)}
|
||||||
</EnvGroup>
|
</EnvGroup>
|
||||||
|
|
||||||
@ -394,18 +390,18 @@ export default function EnvVarsPage() {
|
|||||||
{deploymentVars.map((variable, index) => renderEnvVarRow('development', variable, index))}
|
{deploymentVars.map((variable, index) => renderEnvVarRow('development', variable, index))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-gray-400">No variables defined</p>
|
<p className="text-sm text-muted-foreground">No variables defined</p>
|
||||||
)}
|
)}
|
||||||
</EnvGroup>
|
</EnvGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<button
|
<button
|
||||||
className="px-4 py-2 rounded-md bg-gray-800 hover:bg-gray-700 transition-colors"
|
className="px-4 py-2 rounded-md bg-accent hover:bg-accent/90 transition-colors text-accent-foreground"
|
||||||
onClick={saveChanges}
|
onClick={saveChanges}
|
||||||
disabled={isSaving}
|
disabled={isSaving}
|
||||||
>
|
>
|
||||||
Save changes
|
{isSaving ? "Saving..." : "Save changes"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,47 +1,26 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useRouter, useParams } from "next/navigation";
|
import { useSearchParams, useRouter } from "next/navigation";
|
||||||
import { Clipboard } from "lucide-react";
|
import { Clipboard } from "lucide-react";
|
||||||
import { Dropdown } from "@/components/core/dropdown";
|
import { Dropdown } from "@/components/core/dropdown";
|
||||||
import { LoadingOverlay } from "@/components/foundation/loading/loading-overlay";
|
import { LoadingOverlay } from "@/components/foundation/loading/loading-overlay";
|
||||||
import { useRepoData } from "@/hooks/useRepoData";
|
import { useGQLClient } from "@/context";
|
||||||
|
import type { Project } from '@workspace/gql-client';
|
||||||
|
import { Button } from '@workspace/ui/components/button';
|
||||||
|
import { Input } from '@workspace/ui/components/input';
|
||||||
|
import { Label } from '@workspace/ui/components/label';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@workspace/ui/components/dialog';
|
||||||
|
|
||||||
// Create a simple modal component
|
interface ProjectSettingsPageProps {
|
||||||
interface ModalProps {
|
project?: Project;
|
||||||
isOpen: boolean;
|
onProjectUpdated?: () => void;
|
||||||
onClose: () => void;
|
|
||||||
title: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
footer?: React.ReactNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Modal({ isOpen, onClose, title, children, footer }: ModalProps) {
|
export default function ProjectSettingsPage({ project, onProjectUpdated }: ProjectSettingsPageProps) {
|
||||||
if (!isOpen) return null;
|
const client = useGQLClient();
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
|
|
||||||
<div className="bg-gray-900 border border-gray-700 rounded-lg w-full max-w-md p-6">
|
|
||||||
<div className="flex justify-between items-center mb-4">
|
|
||||||
<h3 className="text-lg font-medium">{title}</h3>
|
|
||||||
<button onClick={onClose} className="text-gray-400 hover:text-white">
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="mb-6">{children}</div>
|
|
||||||
{footer && <div className="flex justify-end">{footer}</div>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ProjectSettingsPage() {
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const searchParams = useSearchParams();
|
||||||
const id = params?.id ? String(params.id) : '';
|
|
||||||
|
|
||||||
// Use the hook to get repo data
|
|
||||||
const { repoData, isLoading } = useRepoData(id);
|
|
||||||
|
|
||||||
const [projectName, setProjectName] = useState("");
|
const [projectName, setProjectName] = useState("");
|
||||||
const [projectDescription, setProjectDescription] = useState("");
|
const [projectDescription, setProjectDescription] = useState("");
|
||||||
@ -51,70 +30,111 @@ export default function ProjectSettingsPage() {
|
|||||||
const [isTransferring, setIsTransferring] = useState(false);
|
const [isTransferring, setIsTransferring] = useState(false);
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
|
const [successMessage, setSuccessMessage] = useState("");
|
||||||
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
|
|
||||||
// Update form values when project data is loaded
|
// Update form values when project data is loaded
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (repoData) {
|
if (project) {
|
||||||
setProjectName(repoData.name || "");
|
setProjectName(project.name || "");
|
||||||
setProjectDescription(repoData.description || "");
|
setProjectDescription(project.description || "");
|
||||||
setProjectId(repoData.id?.toString() || "");
|
setProjectId(project.id || "");
|
||||||
}
|
}
|
||||||
}, [repoData]);
|
}, [project]);
|
||||||
|
|
||||||
|
// Check for delete action in URL params
|
||||||
|
useEffect(() => {
|
||||||
|
const action = searchParams?.get('action');
|
||||||
|
if (action === 'delete' && project) {
|
||||||
|
setIsDeleteModalOpen(true);
|
||||||
|
// Clean up the URL by removing the query parameter
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.delete('action');
|
||||||
|
router.replace(url.pathname);
|
||||||
|
}
|
||||||
|
}, [searchParams, project, router]);
|
||||||
|
|
||||||
const accountOptions = [
|
const accountOptions = [
|
||||||
{ label: "Personal Account", value: "account1" },
|
{ label: "Personal Account", value: "account1" },
|
||||||
{ label: "Team Account", value: "account2" }
|
{ label: "Team Account", value: "account2" }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const showMessage = (message: string, isError = false) => {
|
||||||
|
if (isError) {
|
||||||
|
setErrorMessage(message);
|
||||||
|
setSuccessMessage("");
|
||||||
|
} else {
|
||||||
|
setSuccessMessage(message);
|
||||||
|
setErrorMessage("");
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
setSuccessMessage("");
|
||||||
|
setErrorMessage("");
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
|
if (!project) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
console.log("Saving project info:", { projectName, projectDescription });
|
|
||||||
|
|
||||||
// Simulate API call
|
await client.updateProject(project.id, {
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
name: projectName,
|
||||||
|
description: projectDescription
|
||||||
|
});
|
||||||
|
|
||||||
// Show success notification - in a real app you'd use a toast library
|
showMessage("Project updated successfully");
|
||||||
console.log("Project updated successfully");
|
if (onProjectUpdated) {
|
||||||
|
onProjectUpdated();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to save project info:", error);
|
console.error("Failed to save project info:", error);
|
||||||
// Show error notification
|
showMessage("Failed to update project", true);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTransfer = async () => {
|
const handleTransfer = async () => {
|
||||||
|
if (!project) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsTransferring(true);
|
setIsTransferring(true);
|
||||||
// Transfer project to selected account
|
|
||||||
|
// TODO: Implement actual transfer API call when available
|
||||||
console.log("Transferring project to:", selectedAccount);
|
console.log("Transferring project to:", selectedAccount);
|
||||||
// Implement API call to transfer project
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API call
|
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API call
|
||||||
|
|
||||||
// After successful transfer, navigate back to projects list
|
showMessage("Project transfer initiated successfully");
|
||||||
router.push("/dashboard/projects");
|
// Note: No navigation - staying in the same tab
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to transfer project:", error);
|
console.error("Failed to transfer project:", error);
|
||||||
// Show error notification
|
showMessage("Failed to transfer project", true);
|
||||||
} finally {
|
} finally {
|
||||||
setIsTransferring(false);
|
setIsTransferring(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
|
if (!project) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsDeleting(true);
|
setIsDeleting(true);
|
||||||
// Delete project
|
|
||||||
console.log("Deleting project");
|
|
||||||
// Implement API call to delete project
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API call
|
|
||||||
|
|
||||||
// After successful deletion, navigate back to projects list
|
await client.deleteProject(project.id);
|
||||||
router.push("/dashboard/projects");
|
|
||||||
|
showMessage("Project deleted successfully");
|
||||||
|
setIsDeleteModalOpen(false);
|
||||||
|
|
||||||
|
// Navigate back to projects list after successful deletion
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push('/projects');
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to delete project:", error);
|
console.error("Failed to delete project:", error);
|
||||||
// Show error notification
|
showMessage("Failed to delete project", true);
|
||||||
} finally {
|
} finally {
|
||||||
setIsDeleting(false);
|
setIsDeleting(false);
|
||||||
setIsDeleteModalOpen(false);
|
setIsDeleteModalOpen(false);
|
||||||
@ -123,29 +143,15 @@ export default function ProjectSettingsPage() {
|
|||||||
|
|
||||||
const copyToClipboard = (text: string) => {
|
const copyToClipboard = (text: string) => {
|
||||||
navigator.clipboard.writeText(text);
|
navigator.clipboard.writeText(text);
|
||||||
|
showMessage("Project ID copied to clipboard");
|
||||||
};
|
};
|
||||||
|
|
||||||
const DeleteModalFooter = (
|
if (!project) {
|
||||||
<div className="flex space-x-2">
|
return (
|
||||||
<button
|
<div className="p-6 text-center text-muted-foreground">
|
||||||
className="px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded-md text-white"
|
No project data available
|
||||||
onClick={() => setIsDeleteModalOpen(false)}
|
</div>
|
||||||
disabled={isDeleting}
|
);
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="px-4 py-2 bg-red-600 hover:bg-red-700 rounded-md text-white"
|
|
||||||
onClick={handleDelete}
|
|
||||||
disabled={isDeleting}
|
|
||||||
>
|
|
||||||
{isDeleting ? "Deleting..." : "Delete"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <LoadingOverlay />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -153,47 +159,61 @@ export default function ProjectSettingsPage() {
|
|||||||
{(isSaving || isTransferring || isDeleting) && <LoadingOverlay />}
|
{(isSaving || isTransferring || isDeleting) && <LoadingOverlay />}
|
||||||
|
|
||||||
<div className="space-y-8 w-full">
|
<div className="space-y-8 w-full">
|
||||||
<div className="rounded-lg border border-gray-800 p-6 bg-black">
|
{/* Success/Error Messages */}
|
||||||
<h2 className="text-xl font-semibold mb-4">Project Info</h2>
|
{successMessage && (
|
||||||
|
<div className="bg-green-500/10 text-green-500 p-3 rounded-md border border-green-500/20">
|
||||||
|
{successMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{errorMessage && (
|
||||||
|
<div className="bg-destructive/10 text-destructive p-3 rounded-md border border-destructive/20">
|
||||||
|
{errorMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Project Info Section */}
|
||||||
|
<div className="rounded-lg border border-border p-6 bg-card">
|
||||||
|
<h2 className="text-xl font-semibold mb-4 text-card-foreground">Project Info</h2>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="appName" className="block text-sm font-medium mb-1">
|
<Label htmlFor="appName" className="text-card-foreground">
|
||||||
App name
|
App name
|
||||||
</label>
|
</Label>
|
||||||
<input
|
<Input
|
||||||
id="appName"
|
id="appName"
|
||||||
value={projectName}
|
value={projectName}
|
||||||
onChange={(e) => setProjectName(e.target.value)}
|
onChange={(e) => setProjectName(e.target.value)}
|
||||||
className="w-full px-3 py-2 rounded-md bg-gray-900 border border-gray-700 text-white"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="description" className="block text-sm font-medium mb-1">
|
<Label htmlFor="description" className="text-card-foreground">
|
||||||
Description
|
Description
|
||||||
</label>
|
</Label>
|
||||||
<input
|
<Input
|
||||||
id="description"
|
id="description"
|
||||||
value={projectDescription}
|
value={projectDescription}
|
||||||
onChange={(e) => setProjectDescription(e.target.value)}
|
onChange={(e) => setProjectDescription(e.target.value)}
|
||||||
className="w-full px-3 py-2 rounded-md bg-gray-900 border border-gray-700 text-white"
|
placeholder="Enter project description"
|
||||||
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="projectId" className="block text-sm font-medium mb-1">
|
<Label htmlFor="projectId" className="text-card-foreground">
|
||||||
Project ID
|
Project ID
|
||||||
</label>
|
</Label>
|
||||||
<div className="relative">
|
<div className="relative mt-1">
|
||||||
<input
|
<Input
|
||||||
id="projectId"
|
id="projectId"
|
||||||
value={projectId}
|
value={projectId}
|
||||||
readOnly
|
readOnly
|
||||||
className="w-full px-3 py-2 rounded-md bg-gray-900 border border-gray-700 text-white pr-10"
|
className="pr-10 bg-muted"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
className="absolute right-2 top-1/2 transform -translate-y-1/2"
|
className="absolute right-2 top-1/2 transform -translate-y-1/2 hover:text-foreground text-muted-foreground transition-colors"
|
||||||
onClick={() => copyToClipboard(projectId)}
|
onClick={() => copyToClipboard(projectId)}
|
||||||
aria-label="Copy project ID"
|
aria-label="Copy project ID"
|
||||||
>
|
>
|
||||||
@ -202,73 +222,132 @@ export default function ProjectSettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<div>
|
||||||
className="px-4 py-2 border border-gray-600 rounded-md hover:bg-gray-800 transition-colors mt-2"
|
<Label className="text-card-foreground">Repository</Label>
|
||||||
|
<Input
|
||||||
|
value={project.repository || 'No repository linked'}
|
||||||
|
readOnly
|
||||||
|
className="mt-1 bg-muted"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-card-foreground">Framework</Label>
|
||||||
|
<Input
|
||||||
|
value={project.framework || 'Unknown'}
|
||||||
|
readOnly
|
||||||
|
className="mt-1 bg-muted"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-card-foreground">Production Branch</Label>
|
||||||
|
<Input
|
||||||
|
value={project.prodBranch || 'main'}
|
||||||
|
readOnly
|
||||||
|
className="mt-1 bg-muted"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-card-foreground">Organization</Label>
|
||||||
|
<Input
|
||||||
|
value={project.organization?.name || 'Unknown'}
|
||||||
|
readOnly
|
||||||
|
className="mt-1 bg-muted"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={isSaving}
|
disabled={isSaving}
|
||||||
|
className="mt-4"
|
||||||
>
|
>
|
||||||
{isSaving ? "Saving..." : "Save"}
|
{isSaving ? "Saving..." : "Save"}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-lg border border-gray-800 p-6 bg-black">
|
{/* Transfer Project Section */}
|
||||||
<h2 className="text-xl font-semibold mb-4">Transfer Project</h2>
|
<div className="rounded-lg border border-border p-6 bg-card">
|
||||||
|
<h2 className="text-xl font-semibold mb-4 text-card-foreground">Transfer Project</h2>
|
||||||
|
|
||||||
<div>
|
<div className="space-y-4">
|
||||||
<label htmlFor="account" className="block text-sm font-medium mb-1">
|
<div>
|
||||||
Select account
|
<Label htmlFor="account" className="text-card-foreground">
|
||||||
</label>
|
Select account
|
||||||
<Dropdown
|
</Label>
|
||||||
label="Select"
|
<Dropdown
|
||||||
options={accountOptions}
|
label="Select"
|
||||||
selectedValue={selectedAccount}
|
options={accountOptions}
|
||||||
onSelect={(value) => setSelectedAccount(value)}
|
selectedValue={selectedAccount}
|
||||||
className="w-full"
|
onSelect={(value) => setSelectedAccount(value)}
|
||||||
/>
|
className="w-full mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p className="text-sm text-gray-400 mt-2">
|
<p className="text-sm text-muted-foreground">
|
||||||
Transfer this app to your personal account or a team you are a member of.
|
Transfer this app to your personal account or a team you are a member of.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
className="px-4 py-2 border border-gray-600 rounded-md hover:bg-gray-800 transition-colors mt-4"
|
variant="outline"
|
||||||
onClick={handleTransfer}
|
onClick={handleTransfer}
|
||||||
disabled={!selectedAccount || isTransferring}
|
disabled={!selectedAccount || isTransferring}
|
||||||
>
|
>
|
||||||
{isTransferring ? "Transferring..." : "Transfer"}
|
{isTransferring ? "Transferring..." : "Transfer"}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-lg border border-gray-800 border-red-900 p-6 bg-black">
|
{/* Delete Project Section */}
|
||||||
<h2 className="text-xl font-semibold mb-4 text-red-500">Delete Project</h2>
|
<div className="rounded-lg border border-destructive/50 p-6 bg-card">
|
||||||
|
<h2 className="text-xl font-semibold mb-4 text-destructive">Delete Project</h2>
|
||||||
|
|
||||||
<p className="text-sm text-gray-400 mb-4">
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
The project will be permanently deleted, including its deployments and domains. This action is
|
The project will be permanently deleted, including its deployments and domains. This action is
|
||||||
irreversible and cannot be undone.
|
irreversible and cannot be undone.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
className="px-4 py-2 bg-red-700 hover:bg-red-800 rounded-md text-white transition-colors"
|
variant="destructive"
|
||||||
onClick={() => setIsDeleteModalOpen(true)}
|
onClick={() => setIsDeleteModalOpen(true)}
|
||||||
>
|
>
|
||||||
Delete project
|
Delete project
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<Modal
|
|
||||||
isOpen={isDeleteModalOpen}
|
|
||||||
onClose={() => !isDeleting && setIsDeleteModalOpen(false)}
|
|
||||||
title="Are you absolutely sure?"
|
|
||||||
footer={DeleteModalFooter}
|
|
||||||
>
|
|
||||||
<p className="text-gray-300">
|
|
||||||
This action cannot be undone. This will permanently delete the project
|
|
||||||
and all associated deployments and domains.
|
|
||||||
</p>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Delete Confirmation Modal */}
|
||||||
|
<Dialog open={isDeleteModalOpen} onOpenChange={(open) => !isDeleting && setIsDeleteModalOpen(open)}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="py-4">
|
||||||
|
<p className="text-foreground">
|
||||||
|
This action cannot be undone. This will permanently delete the project{" "}
|
||||||
|
<strong>"{project.name}"</strong> and all associated deployments and domains.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsDeleteModalOpen(false)}
|
||||||
|
disabled={isDeleting}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={handleDelete}
|
||||||
|
disabled={isDeleting}
|
||||||
|
>
|
||||||
|
{isDeleting ? "Deleting..." : "Delete"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,21 +1,56 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useParams } from 'next/navigation';
|
import { useParams } from 'next/navigation';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { PageWrapper } from "@/components/foundation";
|
import { PageWrapper } from "@/components/foundation";
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||||
import ProjectSettingsPage from "./ProjectSettingsPage";
|
import ProjectSettingsPage from "./ProjectSettingsPage";
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useRepoData } from '@/hooks/useRepoData';
|
import { useGQLClient } from '@/context';
|
||||||
|
import type { Project } from '@workspace/gql-client';
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
const client = useGQLClient();
|
||||||
|
|
||||||
// Safely unwrap params
|
// Safely unwrap params
|
||||||
const id = params?.id ? String(params.id) : '';
|
const id = params?.id ? String(params.id) : '';
|
||||||
const provider = params?.provider ? String(params.provider) : '';
|
const provider = params?.provider ? String(params.provider) : '';
|
||||||
|
|
||||||
// Use the hook to get repo data
|
// State for project data
|
||||||
const { repoData } = useRepoData(id);
|
const [project, setProject] = useState<Project | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Load project data using GraphQL client
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
loadProject(id);
|
||||||
|
}
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const loadProject = async (projectId: string) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const response = await client.getProject(projectId);
|
||||||
|
setProject(response.project);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load project:', err);
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to load project');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle project updates
|
||||||
|
const handleProjectUpdated = () => {
|
||||||
|
if (id) {
|
||||||
|
loadProject(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Handle tab changes by navigating to the correct folder
|
// Handle tab changes by navigating to the correct folder
|
||||||
const handleTabChange = (value: string) => {
|
const handleTabChange = (value: string) => {
|
||||||
@ -39,21 +74,60 @@ export default function SettingsPage() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<PageWrapper
|
||||||
|
header={{
|
||||||
|
title: 'Loading...',
|
||||||
|
actions: []
|
||||||
|
}}
|
||||||
|
layout="bento"
|
||||||
|
className="pb-0"
|
||||||
|
>
|
||||||
|
<div className="md:col-span-3 w-full flex items-center justify-center py-12">
|
||||||
|
<div className="text-muted-foreground">Loading project settings...</div>
|
||||||
|
</div>
|
||||||
|
</PageWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error state
|
||||||
|
if (error || !project) {
|
||||||
|
return (
|
||||||
|
<PageWrapper
|
||||||
|
header={{
|
||||||
|
title: 'Error',
|
||||||
|
actions: []
|
||||||
|
}}
|
||||||
|
layout="bento"
|
||||||
|
className="pb-0"
|
||||||
|
>
|
||||||
|
<div className="md:col-span-3 w-full flex items-center justify-center py-12">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-destructive mb-2">Failed to load project</div>
|
||||||
|
<div className="text-muted-foreground text-sm">{error}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper
|
<PageWrapper
|
||||||
header={{
|
header={{
|
||||||
title: repoData ? `${repoData.name}` : 'Project Settings',
|
title: project.name || 'Project Settings',
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
label: 'Open repo',
|
label: 'Open repo',
|
||||||
href: repoData?.html_url || '#',
|
href: project.repository || '#',
|
||||||
icon: 'external-link',
|
icon: 'external-link',
|
||||||
external: true
|
external: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'View app',
|
label: 'View app',
|
||||||
href: repoData ? `https://${repoData.name.toLowerCase()}.example.com` : '#',
|
href: project.deployments?.[0]?.applicationDeploymentRecordData?.url || '#',
|
||||||
icon: 'external-link',
|
icon: 'external-link',
|
||||||
external: true
|
external: true
|
||||||
}
|
}
|
||||||
@ -74,9 +148,12 @@ export default function SettingsPage() {
|
|||||||
</TabsList>
|
</TabsList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
{/* Settings content */}
|
{/* Settings content - now with proper project data */}
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<ProjectSettingsPage />
|
<ProjectSettingsPage
|
||||||
|
project={project}
|
||||||
|
onProjectUpdated={handleProjectUpdated}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
|
|||||||
@ -9,119 +9,325 @@ import {
|
|||||||
AvatarFallback} from '@workspace/ui/components/avatar';
|
AvatarFallback} from '@workspace/ui/components/avatar';
|
||||||
import { Button } from '@workspace/ui/components/button';
|
import { Button } from '@workspace/ui/components/button';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||||
import { Activity, Clock, GitBranch, ExternalLink } from 'lucide-react';
|
import { Activity, Clock, GitBranch, ExternalLink, AlertCircle, Square, Search, Calendar, ChevronDown, Clipboard } from 'lucide-react';
|
||||||
|
import { Badge } from '@workspace/ui/components/badge';
|
||||||
|
import { Input } from '@workspace/ui/components/input';
|
||||||
|
import { Label } from '@workspace/ui/components/label';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@workspace/ui/components/dialog';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useRepoData } from '@/hooks/useRepoData';
|
import { useGQLClient } from '@/context';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import type { Project } from '@workspace/gql-client';
|
||||||
|
|
||||||
|
// Import the tab content components
|
||||||
|
import GitPage from './(integrations)/int/GitPage';
|
||||||
|
import EnvVarsPage from './(settings)/set/(environment-variables)/env/EnvVarsPage';
|
||||||
|
|
||||||
export default function ProjectOverviewPage() {
|
export default function ProjectOverviewPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
const client = useGQLClient();
|
||||||
|
|
||||||
// Safely unwrap params
|
// Safely unwrap params
|
||||||
const id = params?.id ? String(params.id) : '';
|
const id = params?.id ? String(params.id) : '';
|
||||||
const provider = params?.provider ? String(params.provider) : '';
|
|
||||||
|
|
||||||
// Use the hook to get repo data
|
// State management
|
||||||
const { repoData } = useRepoData(id);
|
const [project, setProject] = useState<Project | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [activeTab, setActiveTab] = useState('overview'); // Local tab state
|
||||||
|
|
||||||
// Default deployment details
|
// Deployment page state
|
||||||
const [deploymentUrl, setDeploymentUrl] = useState('');
|
const [deployments, setDeployments] = useState<any[]>([]);
|
||||||
const [deploymentDate, setDeploymentDate] = useState(Date.now() - 60 * 60 * 1000); // 1 hour ago
|
const [filteredDeployments, setFilteredDeployments] = useState<any[]>([]);
|
||||||
const [deployedBy, setDeployedBy] = useState('');
|
const [isLogsOpen, setIsLogsOpen] = useState(false);
|
||||||
const [projectName, setProjectName] = useState('');
|
const [deploymentLogs, setDeploymentLogs] = useState<string>('');
|
||||||
const [branch, setBranch] = useState('main');
|
|
||||||
|
// Load project data
|
||||||
// Update details when repo data is loaded
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (repoData) {
|
if (id) {
|
||||||
setProjectName(repoData.name);
|
loadProject(id);
|
||||||
setBranch(repoData.default_branch || 'main');
|
|
||||||
setDeployedBy(repoData.owner?.login || 'username');
|
|
||||||
// Create a deployment URL based on the repo name
|
|
||||||
setDeploymentUrl(`https://${repoData.name.toLowerCase()}.example.com`);
|
|
||||||
}
|
}
|
||||||
}, [repoData]);
|
}, [id]);
|
||||||
|
|
||||||
// Auction data
|
|
||||||
const auctionId = 'laconic1sdfjwei4jfkasifgjiai45ioasjf5jjjafij355';
|
|
||||||
|
|
||||||
// Activities data
|
// Load project from GraphQL
|
||||||
const activities = [
|
const loadProject = async (projectId: string) => {
|
||||||
{
|
try {
|
||||||
username: deployedBy || 'username',
|
setLoading(true);
|
||||||
branch: branch,
|
setError(null);
|
||||||
action: 'deploy: source cargo',
|
|
||||||
time: '5 minutes ago'
|
const response = await client.getProject(projectId);
|
||||||
},
|
setProject(response.project);
|
||||||
{
|
|
||||||
username: deployedBy || 'username',
|
// Set deployments for the deployment tab
|
||||||
branch: branch,
|
if (response.project?.deployments) {
|
||||||
action: 'bump',
|
setDeployments(response.project.deployments);
|
||||||
time: '5 minutes ago'
|
setFilteredDeployments(response.project.deployments);
|
||||||
},
|
}
|
||||||
{
|
} catch (err) {
|
||||||
username: deployedBy || 'username',
|
console.error('Failed to load project:', err);
|
||||||
branch: branch,
|
setError(err instanceof Error ? err.message : 'Failed to load project');
|
||||||
action: 'version: update version',
|
} finally {
|
||||||
time: '5 minutes ago'
|
setLoading(false);
|
||||||
},
|
|
||||||
{
|
|
||||||
username: deployedBy || 'username',
|
|
||||||
branch: branch,
|
|
||||||
action: 'build: updates',
|
|
||||||
time: '5 minutes ago'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// Handle tab changes by navigating to the correct folder
|
|
||||||
const handleTabChange = (value: string) => {
|
|
||||||
const basePath = `/projects/${provider}/ps/${id}`;
|
|
||||||
|
|
||||||
switch (value) {
|
|
||||||
case 'overview':
|
|
||||||
router.push(basePath);
|
|
||||||
break;
|
|
||||||
case 'deployment':
|
|
||||||
router.push(`${basePath}/dep`);
|
|
||||||
break;
|
|
||||||
case 'settings':
|
|
||||||
router.push(`${basePath}/set`);
|
|
||||||
break;
|
|
||||||
case 'git':
|
|
||||||
router.push(`${basePath}/int`);
|
|
||||||
break;
|
|
||||||
case 'env-vars':
|
|
||||||
router.push(`${basePath}/set/env`);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Refresh project data
|
||||||
|
const handleRefresh = () => {
|
||||||
|
if (id) {
|
||||||
|
loadProject(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentDeployment = project?.deployments?.find((d: any) => d.isCurrent);
|
||||||
|
const latestDeployment = project?.deployments?.[0]; // Assuming deployments are sorted by date
|
||||||
|
|
||||||
|
// Handle tab changes WITHOUT navigation - just update local state
|
||||||
|
const handleTabChange = (value: string) => {
|
||||||
|
setActiveTab(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to safely parse dates
|
||||||
|
const parseDate = (dateString: string | undefined) => {
|
||||||
|
if (!dateString) return null;
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return isNaN(date.getTime()) ? null : date.getTime();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate activities from deployments
|
||||||
|
const generateActivities = () => {
|
||||||
|
if (!project?.deployments) return [];
|
||||||
|
|
||||||
|
return project.deployments
|
||||||
|
.slice(0, 4) // Show last 4 deployments
|
||||||
|
.map((deployment: any) => ({
|
||||||
|
username: deployment.createdBy?.name || 'Unknown',
|
||||||
|
branch: deployment.branch,
|
||||||
|
action: `deployed ${deployment.environment || 'production'}`,
|
||||||
|
time: parseDate(deployment.createdAt) ? relativeTimeMs(parseDate(deployment.createdAt)!) : 'Unknown time',
|
||||||
|
status: deployment.status
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const activities = generateActivities();
|
||||||
|
|
||||||
|
// Status badge component
|
||||||
|
const StatusBadge = ({ status }: { status: string }) => {
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status?.toUpperCase()) {
|
||||||
|
case 'COMPLETED':
|
||||||
|
case 'READY':
|
||||||
|
return 'bg-green-700/20 text-green-400';
|
||||||
|
case 'BUILDING':
|
||||||
|
case 'DEPLOYING':
|
||||||
|
return 'bg-blue-700/20 text-blue-400';
|
||||||
|
case 'ERROR':
|
||||||
|
case 'FAILED':
|
||||||
|
return 'bg-red-700/20 text-red-400';
|
||||||
|
default:
|
||||||
|
return 'bg-gray-700/20 text-gray-400';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`inline-block px-2 py-0.5 text-xs font-medium rounded ${getStatusColor(status)}`}>
|
||||||
|
{status?.toUpperCase() || 'UNKNOWN'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle deployment logs
|
||||||
|
const handleViewLogs = () => {
|
||||||
|
const mockLogs = `[2025-02-12 10:03:12] INFO Starting deployment process for service: ${project?.name}
|
||||||
|
[2025-02-12 10:03:14] INFO Fetching latest commit from main branch (commit: a1b2c3d)
|
||||||
|
[2025-02-12 10:03:15] INFO Building Docker image: registry.company.com/${project?.name}:latest
|
||||||
|
[2025-02-12 10:03:26] INFO Running security scan on built image
|
||||||
|
[2025-02-12 10:03:30] INFO Pushing image to container registry
|
||||||
|
[2025-02-12 10:03:35] INFO Updating deployment configuration
|
||||||
|
[2025-02-12 10:03:40] INFO Scaling down old pods
|
||||||
|
[2025-02-12 10:03:42] INFO Scaling up new pods
|
||||||
|
[2025-02-12 10:03:50] INFO Running health checks on new pods
|
||||||
|
[2025-02-12 10:03:55] INFO Deployment completed successfully
|
||||||
|
[2025-02-12 10:03:56] INFO Service is now live at ${currentDeployment?.applicationDeploymentRecordData?.url}`;
|
||||||
|
|
||||||
|
setDeploymentLogs(mockLogs);
|
||||||
|
setIsLogsOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle deployment deletion
|
||||||
|
const handleDeleteDeployment = async (deploymentId: string, deploymentBranch: string) => {
|
||||||
|
if (!confirm(`Are you sure you want to delete the deployment for branch "${deploymentBranch}"? This action cannot be undone.`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
await client.deleteDeployment(deploymentId);
|
||||||
|
|
||||||
|
// Refresh the project data to update the deployments list
|
||||||
|
await loadProject(id);
|
||||||
|
|
||||||
|
// Show success message (you could replace with a toast notification)
|
||||||
|
alert('Deployment deleted successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete deployment:', error);
|
||||||
|
alert('Failed to delete deployment. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle deployment rollback
|
||||||
|
const handleRollbackDeployment = async (deploymentId: string, deploymentBranch: string) => {
|
||||||
|
if (!project?.id) return;
|
||||||
|
|
||||||
|
if (!confirm(`Are you sure you want to rollback to the deployment for branch "${deploymentBranch}"?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
await client.rollbackDeployment(project.id, deploymentId);
|
||||||
|
|
||||||
|
// Refresh the project data to update the deployments list
|
||||||
|
await loadProject(id);
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
alert('Deployment rollback completed successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to rollback deployment:', error);
|
||||||
|
alert('Failed to rollback deployment. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Copy to clipboard function for settings
|
||||||
|
const copyToClipboard = (text: string) => {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
// You could add a toast notification here
|
||||||
|
};
|
||||||
|
|
||||||
|
// Settings page state
|
||||||
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
const [successMessage, setSuccessMessage] = useState("");
|
||||||
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
|
|
||||||
|
const showMessage = (message: string, isError = false) => {
|
||||||
|
if (isError) {
|
||||||
|
setErrorMessage(message);
|
||||||
|
setSuccessMessage("");
|
||||||
|
} else {
|
||||||
|
setSuccessMessage(message);
|
||||||
|
setErrorMessage("");
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
setSuccessMessage("");
|
||||||
|
setErrorMessage("");
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle project deletion
|
||||||
|
const handleDeleteProject = async () => {
|
||||||
|
if (!project) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsDeleting(true);
|
||||||
|
|
||||||
|
await client.deleteProject(project.id);
|
||||||
|
|
||||||
|
showMessage("Project deleted successfully");
|
||||||
|
setIsDeleteModalOpen(false);
|
||||||
|
|
||||||
|
// Navigate back to projects list after successful deletion
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push('/projects');
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete project:", error);
|
||||||
|
showMessage("Failed to delete project", true);
|
||||||
|
} finally {
|
||||||
|
setIsDeleting(false);
|
||||||
|
setIsDeleteModalOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Loading state
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<PageWrapper
|
||||||
|
header={{
|
||||||
|
title: 'Loading...',
|
||||||
|
actions: []
|
||||||
|
}}
|
||||||
|
layout="bento"
|
||||||
|
className="pb-0"
|
||||||
|
>
|
||||||
|
<div className="md:col-span-3 w-full flex items-center justify-center py-12">
|
||||||
|
<div className="text-muted-foreground">Loading project data...</div>
|
||||||
|
</div>
|
||||||
|
</PageWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error state
|
||||||
|
if (error || !project) {
|
||||||
|
return (
|
||||||
|
<PageWrapper
|
||||||
|
header={{
|
||||||
|
title: 'Project Not Found',
|
||||||
|
actions: []
|
||||||
|
}}
|
||||||
|
layout="bento"
|
||||||
|
className="pb-0"
|
||||||
|
>
|
||||||
|
<div className="md:col-span-3 w-full flex flex-col items-center justify-center py-12">
|
||||||
|
<AlertCircle className="h-12 w-12 text-red-500 mb-4" />
|
||||||
|
<div className="text-xl font-medium mb-2">Project not found</div>
|
||||||
|
<div className="text-muted-foreground mb-4">
|
||||||
|
{error ? `Error: ${error}` : 'The requested project could not be loaded.'}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="outline" onClick={() => router.back()}>
|
||||||
|
Go Back
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleRefresh}>
|
||||||
|
Retry
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper
|
<PageWrapper
|
||||||
header={{
|
header={{
|
||||||
title: projectName || 'Project Overview',
|
title: project.name || 'Project Overview',
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
label: 'Open repo',
|
label: 'Open repo',
|
||||||
href: repoData?.html_url || '#',
|
href: project.repository || '#',
|
||||||
icon: 'external-link',
|
icon: 'external-link',
|
||||||
external: true
|
external: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'View app',
|
label: 'View app',
|
||||||
href: deploymentUrl || '#',
|
href: currentDeployment?.applicationDeploymentRecordData?.url || latestDeployment?.applicationDeploymentRecordData?.url || '#',
|
||||||
icon: 'external-link',
|
icon: 'external-link',
|
||||||
external: true
|
external: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}}
|
}}
|
||||||
layout="bento" // Use bento layout to override max width
|
layout="bento"
|
||||||
className="pb-0"
|
className="pb-0"
|
||||||
>
|
>
|
||||||
<div className="md:col-span-3 w-full"> {/* Take full width in bento grid */}
|
<div className="md:col-span-3 w-full">
|
||||||
{/* Tabs navigation */}
|
{/* Tabs navigation - controlled locally */}
|
||||||
<Tabs defaultValue="overview" className="w-full" onValueChange={handleTabChange}>
|
<Tabs value={activeTab} onValueChange={handleTabChange} className="w-full">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||||
<TabsTrigger value="deployment">Deployment</TabsTrigger>
|
<TabsTrigger value="deployment">Deployment</TabsTrigger>
|
||||||
@ -137,13 +343,23 @@ export default function ProjectOverviewPage() {
|
|||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Avatar className="h-10 w-10 mr-4 bg-blue-600">
|
<Avatar className="h-10 w-10 mr-4 bg-blue-600">
|
||||||
<AvatarFallback>{getInitials(projectName || '')}</AvatarFallback>
|
<AvatarFallback>{getInitials(project.name || '')}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div>
|
<div className="flex-1">
|
||||||
<h2 className="text-lg font-medium">{projectName}</h2>
|
<div className="flex items-center gap-3">
|
||||||
|
<h2 className="text-lg font-medium">{project.name}</h2>
|
||||||
|
{currentDeployment && (
|
||||||
|
<StatusBadge status={currentDeployment.status} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
{deploymentUrl.replace(/^https?:\/\//, '')}
|
{currentDeployment?.applicationDeploymentRecordData?.url?.replace(/^https?:\/\//, '') ||
|
||||||
|
latestDeployment?.applicationDeploymentRecordData?.url?.replace(/^https?:\/\//, '') ||
|
||||||
|
'No deployment URL'}
|
||||||
</p>
|
</p>
|
||||||
|
{project.description && (
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">{project.description}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -151,78 +367,120 @@ export default function ProjectOverviewPage() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center mb-2">
|
<div className="flex items-center mb-2">
|
||||||
<GitBranch className="h-4 w-4 mr-2 text-muted-foreground" />
|
<GitBranch className="h-4 w-4 mr-2 text-muted-foreground" />
|
||||||
<span className="text-muted-foreground text-sm">Source</span>
|
<span className="text-muted-foreground text-sm">Production Branch</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<GitBranch className="h-4 w-4 mr-2" />
|
<GitBranch className="h-4 w-4 mr-2" />
|
||||||
<span>{branch}</span>
|
<span>{project.prodBranch || 'main'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center mb-2">
|
<div className="flex items-center mb-2">
|
||||||
<ExternalLink className="h-4 w-4 mr-2 text-muted-foreground" />
|
<ExternalLink className="h-4 w-4 mr-2 text-muted-foreground" />
|
||||||
<span className="text-muted-foreground text-sm">Deployment URL</span>
|
<span className="text-muted-foreground text-sm">Repository</span>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
{project.repository ? (
|
||||||
href={deploymentUrl}
|
<Link
|
||||||
className="text-primary hover:underline flex items-center"
|
href={project.repository}
|
||||||
target="_blank"
|
className="text-primary hover:underline flex items-center"
|
||||||
>
|
target="_blank"
|
||||||
{deploymentUrl}
|
>
|
||||||
</Link>
|
{project.repository.replace('https://github.com/', '')}
|
||||||
|
<ExternalLink className="h-3 w-3 ml-1" />
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground">No repository linked</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6">
|
||||||
<div className="flex items-center mb-2">
|
<div>
|
||||||
<Clock className="h-4 w-4 mr-2 text-muted-foreground" />
|
<div className="flex items-center mb-2">
|
||||||
<span className="text-muted-foreground text-sm">Deployment date</span>
|
<Clock className="h-4 w-4 mr-2 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground text-sm">Last Deployment</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="mr-2">
|
||||||
|
{latestDeployment ?
|
||||||
|
(parseDate(latestDeployment.createdAt) ?
|
||||||
|
relativeTimeMs(parseDate(latestDeployment.createdAt)!) :
|
||||||
|
'Invalid date') :
|
||||||
|
'No deployments'
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
{latestDeployment?.createdBy && (
|
||||||
|
<>
|
||||||
|
<span className="mr-2">by</span>
|
||||||
|
<Avatar className="h-5 w-5 mr-2">
|
||||||
|
<AvatarFallback>{getInitials(latestDeployment.createdBy.name || '')}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<span>{latestDeployment.createdBy.name}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="mr-2">
|
<div>
|
||||||
{relativeTimeMs(deploymentDate)}
|
<div className="flex items-center mb-2">
|
||||||
</span>
|
<Activity className="h-4 w-4 mr-2 text-muted-foreground" />
|
||||||
<span className="mr-2">by</span>
|
<span className="text-muted-foreground text-sm">Framework</span>
|
||||||
<Avatar className="h-5 w-5 mr-2">
|
</div>
|
||||||
<AvatarFallback>{getInitials(deployedBy)}</AvatarFallback>
|
<div className="flex items-center">
|
||||||
</Avatar>
|
<Badge variant="secondary">{project.framework || 'Unknown'}</Badge>
|
||||||
<span>{deployedBy}</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Divider between project info and auction details */}
|
{/* Divider between project info and auction details */}
|
||||||
<div className="border-t border-border my-6"></div>
|
<div className="border-t border-border my-6"></div>
|
||||||
|
|
||||||
{/* Auction Details section */}
|
{/* Deployment Details section */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium mb-6">Auction Details</h3>
|
<h3 className="text-lg font-medium mb-6">Deployment Details</h3>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm text-muted-foreground mb-1">Auction ID</h4>
|
<h4 className="text-sm text-muted-foreground mb-1">Project ID</h4>
|
||||||
<p className="text-sm font-medium break-all">{auctionId}</p>
|
<p className="text-sm font-medium font-mono break-all">{project.id}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm text-muted-foreground mb-1">Auction Status</h4>
|
<h4 className="text-sm text-muted-foreground mb-1">Organization</h4>
|
||||||
<div className="inline-block px-2 py-0.5 bg-green-700/20 text-green-400 text-xs font-medium rounded">
|
<p className="text-sm font-medium">{project.organization?.name || 'Unknown'}</p>
|
||||||
COMPLETED
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6">
|
{project.auctionId && (
|
||||||
<div>
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6">
|
||||||
<h4 className="text-sm text-muted-foreground mb-1">Deployer LRNs</h4>
|
<div>
|
||||||
<p className="text-sm font-medium break-all">{auctionId}</p>
|
<h4 className="text-sm text-muted-foreground mb-1">Auction ID</h4>
|
||||||
</div>
|
<p className="text-sm font-medium font-mono break-all">{project.auctionId}</p>
|
||||||
<div>
|
</div>
|
||||||
<h4 className="text-sm text-muted-foreground mb-1">Deployer Funds Status</h4>
|
<div>
|
||||||
<div className="inline-block px-2 py-0.5 bg-blue-700/20 text-blue-400 text-xs font-medium rounded">
|
<h4 className="text-sm text-muted-foreground mb-1">Funds Status</h4>
|
||||||
RELEASED
|
<StatusBadge status={project.fundsReleased ? 'RELEASED' : 'PENDING'} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
|
{project.deployers && project.deployers.length > 0 && (
|
||||||
|
<div className="mt-6">
|
||||||
|
<h4 className="text-sm text-muted-foreground mb-3">Deployers ({project.deployers.length})</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{project.deployers.slice(0, 2).map((deployer: any, index: number) => (
|
||||||
|
<div key={index} className="text-sm font-mono bg-muted p-2 rounded">
|
||||||
|
{deployer.deployerLrn}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{project.deployers.length > 2 && (
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
And {project.deployers.length - 2} more...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<Button variant="outline" size="sm">View details</Button>
|
<Button variant="outline" size="sm">View details</Button>
|
||||||
@ -231,7 +489,7 @@ export default function ProjectOverviewPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Activity section - not in a card */}
|
{/* Activity section */}
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<h3 className="text-lg font-medium mb-6 flex items-center">
|
<h3 className="text-lg font-medium mb-6 flex items-center">
|
||||||
<Activity className="mr-2 h-4 w-4" />
|
<Activity className="mr-2 h-4 w-4" />
|
||||||
@ -239,29 +497,312 @@ export default function ProjectOverviewPage() {
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{activities.map((activity, index) => (
|
{activities.length > 0 ? (
|
||||||
<div key={index} className="flex items-start">
|
activities.map((activity, index) => (
|
||||||
<div className="text-muted-foreground mr-2">•</div>
|
<div key={index} className="flex items-start">
|
||||||
<div className="flex-1">
|
<div className="text-muted-foreground mr-2">•</div>
|
||||||
<span className="text-sm mr-2">{activity.username}</span>
|
<div className="flex-1">
|
||||||
<GitBranch className="inline h-3 w-3 text-muted-foreground mx-1" />
|
<span className="text-sm mr-2">{activity.username}</span>
|
||||||
<span className="text-sm text-muted-foreground mr-2">{activity.branch}</span>
|
<GitBranch className="inline h-3 w-3 text-muted-foreground mx-1" />
|
||||||
<span className="text-sm text-muted-foreground">{activity.action}</span>
|
<span className="text-sm text-muted-foreground mr-2">{activity.branch}</span>
|
||||||
|
<span className="text-sm text-muted-foreground">{activity.action}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">{activity.time}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-muted-foreground">{activity.time}</div>
|
))
|
||||||
|
) : (
|
||||||
|
<div className="text-muted-foreground text-center py-8">
|
||||||
|
No recent activity
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* These content sections won't be shown - we'll navigate to respective pages instead */}
|
<TabsContent value="deployment" className="pt-6">
|
||||||
<TabsContent value="deployment"></TabsContent>
|
<div className="space-y-6">
|
||||||
<TabsContent value="settings"></TabsContent>
|
{/* Filter Controls */}
|
||||||
<TabsContent value="git"></TabsContent>
|
<div className="flex flex-wrap gap-4">
|
||||||
<TabsContent value="env-vars"></TabsContent>
|
<div className="relative flex-grow max-w-md">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<Search className="h-4 w-4 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search branches"
|
||||||
|
className="pl-10"
|
||||||
|
disabled={deployments.length === 0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="flex items-center opacity-60"
|
||||||
|
disabled={deployments.length === 0}
|
||||||
|
>
|
||||||
|
<Calendar className="h-4 w-4 mr-2" />
|
||||||
|
<span>Select a date</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="flex items-center opacity-60"
|
||||||
|
disabled={deployments.length === 0}
|
||||||
|
>
|
||||||
|
<span>All Status</span>
|
||||||
|
<ChevronDown className="h-4 w-4 ml-2" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Deployments List */}
|
||||||
|
{filteredDeployments.length > 0 ? (
|
||||||
|
filteredDeployments.map((deployment) => (
|
||||||
|
<div key={deployment.id} className="border border-border rounded-lg p-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<GitBranch className="h-4 w-4" />
|
||||||
|
<span className="font-medium">{deployment.branch}</span>
|
||||||
|
<StatusBadge status={deployment.status} />
|
||||||
|
{deployment.isCurrent && (
|
||||||
|
<Badge variant="default">Current</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleViewLogs(deployment.id)}
|
||||||
|
>
|
||||||
|
View logs
|
||||||
|
</Button>
|
||||||
|
{!deployment.isCurrent && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleRollbackDeployment(deployment.id, deployment.branch)}
|
||||||
|
>
|
||||||
|
Rollback
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDeleteDeployment(deployment.id, deployment.branch)}
|
||||||
|
disabled={deployment.isCurrent}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">URL: </span>
|
||||||
|
<Link
|
||||||
|
href={deployment.applicationDeploymentRecordData?.url || '#'}
|
||||||
|
className="text-primary hover:underline"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{deployment.applicationDeploymentRecordData?.url}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Created: </span>
|
||||||
|
{parseDate(deployment.createdAt) ?
|
||||||
|
relativeTimeMs(parseDate(deployment.createdAt)!) :
|
||||||
|
'Unknown date'
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Commit: </span>
|
||||||
|
<span className="font-mono">{deployment.commitHash?.substring(0, 8) || 'Unknown'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Created by: </span>
|
||||||
|
{deployment.createdBy?.name || 'Unknown'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="h-96 border border-gray-800 rounded-lg flex flex-col items-center justify-center gap-5 text-center">
|
||||||
|
<Square size={64} className="stroke-current" />
|
||||||
|
<h2 className="text-xl font-semibold mb-2">You have no deployments</h2>
|
||||||
|
<p className="text-gray-400 text-center max-w-md mb-6">
|
||||||
|
Deploy your first version to see deployment history here.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="settings" className="pt-6">
|
||||||
|
{/* Updated theme-aware settings content */}
|
||||||
|
<div className="space-y-8 w-full">
|
||||||
|
{/* Success/Error Messages */}
|
||||||
|
{successMessage && (
|
||||||
|
<div className="bg-green-500/10 text-green-500 p-3 rounded-md border border-green-500/20">
|
||||||
|
{successMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{errorMessage && (
|
||||||
|
<div className="bg-destructive/10 text-destructive p-3 rounded-md border border-destructive/20">
|
||||||
|
{errorMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="rounded-lg border border-border p-6 bg-card">
|
||||||
|
<h2 className="text-xl font-semibold mb-4 text-card-foreground">Project Info</h2>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="appName" className="text-card-foreground">
|
||||||
|
App name
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="appName"
|
||||||
|
value={project.name || ''}
|
||||||
|
readOnly
|
||||||
|
className="mt-1 bg-muted"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="description" className="text-card-foreground">
|
||||||
|
Description
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="description"
|
||||||
|
value={project.description || ''}
|
||||||
|
readOnly
|
||||||
|
className="mt-1 bg-muted"
|
||||||
|
placeholder="No description"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="projectId" className="text-card-foreground">
|
||||||
|
Project ID
|
||||||
|
</Label>
|
||||||
|
<div className="relative mt-1">
|
||||||
|
<Input
|
||||||
|
id="projectId"
|
||||||
|
value={project.id}
|
||||||
|
readOnly
|
||||||
|
className="pr-10 bg-muted"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="absolute right-2 top-1/2 transform -translate-y-1/2 hover:text-foreground text-muted-foreground transition-colors"
|
||||||
|
onClick={() => copyToClipboard(project.id)}
|
||||||
|
aria-label="Copy project ID"
|
||||||
|
>
|
||||||
|
<Clipboard className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-card-foreground">Repository</Label>
|
||||||
|
<Input
|
||||||
|
value={project.repository || 'No repository linked'}
|
||||||
|
readOnly
|
||||||
|
className="mt-1 bg-muted"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-card-foreground">Framework</Label>
|
||||||
|
<Input
|
||||||
|
value={project.framework || 'Unknown'}
|
||||||
|
readOnly
|
||||||
|
className="mt-1 bg-muted"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-card-foreground">Production Branch</Label>
|
||||||
|
<Input
|
||||||
|
value={project.prodBranch || 'main'}
|
||||||
|
readOnly
|
||||||
|
className="mt-1 bg-muted"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-card-foreground">Organization</Label>
|
||||||
|
<Input
|
||||||
|
value={project.organization?.name || 'Unknown'}
|
||||||
|
readOnly
|
||||||
|
className="mt-1 bg-muted"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button variant="outline" disabled className="mt-4">
|
||||||
|
Edit Settings (Coming Soon)
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-lg border border-destructive/50 p-6 bg-card">
|
||||||
|
<h2 className="text-xl font-semibold mb-4 text-destructive">Delete Project</h2>
|
||||||
|
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
The project will be permanently deleted, including its deployments and domains. This action is
|
||||||
|
irreversible and cannot be undone.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => setIsDeleteModalOpen(true)}
|
||||||
|
disabled={isDeleting}
|
||||||
|
>
|
||||||
|
{isDeleting ? "Deleting..." : "Delete Project"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="git" className="pt-6">
|
||||||
|
<GitPage />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="env-vars" className="pt-6">
|
||||||
|
<EnvVarsPage />
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{/* Delete Confirmation Modal */}
|
||||||
|
<Dialog open={isDeleteModalOpen} onOpenChange={(open) => !isDeleting && setIsDeleteModalOpen(open)}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="py-4">
|
||||||
|
<p className="text-foreground">
|
||||||
|
This action cannot be undone. This will permanently delete the project{" "}
|
||||||
|
<strong>"{project?.name}"</strong> and all associated deployments and domains.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsDeleteModalOpen(false)}
|
||||||
|
disabled={isDeleting}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={handleDeleteProject}
|
||||||
|
disabled={isDeleting}
|
||||||
|
>
|
||||||
|
{isDeleting ? "Deleting..." : "Delete"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,72 +1,85 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { PageWrapper } from '@/components/foundation'
|
import { PageWrapper } from '@/components/foundation'
|
||||||
import CheckBalanceIframe from '@/components/iframe/check-balance-iframe/CheckBalanceIframe'
|
import CheckBalanceIframe from '@/components/iframe/check-balance-iframe/CheckBalanceIframe'
|
||||||
import type { Project } from '@octokit/webhooks-types'
|
|
||||||
import { FixedProjectCard } from '@/components/projects/project/ProjectCard/FixedProjectCard'
|
import { FixedProjectCard } from '@/components/projects/project/ProjectCard/FixedProjectCard'
|
||||||
import { Button } from '@workspace/ui/components/button'
|
import { Button } from '@workspace/ui/components/button'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Shapes } from 'lucide-react'
|
import { Shapes } from 'lucide-react'
|
||||||
import { useAuth, useUser } from '@clerk/nextjs'
|
import { useGQLClient } from '@/context'
|
||||||
import { useRepoData } from '@/hooks/useRepoData'
|
import type { Project } from '@workspace/gql-client'
|
||||||
|
|
||||||
interface ProjectData {
|
interface ProjectData {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
icon?: string
|
icon?: string
|
||||||
deployments: any[]
|
deployments: any[]
|
||||||
// Additional fields from GitHub repo
|
repository?: string
|
||||||
full_name?: string
|
framework?: string
|
||||||
html_url?: string
|
description?: string
|
||||||
updated_at?: string
|
|
||||||
default_branch?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProjectsPage() {
|
export default function ProjectsPage() {
|
||||||
const [, setIsBalanceSufficient] = useState<boolean>()
|
const [, setIsBalanceSufficient] = useState<boolean>()
|
||||||
const [projects, setProjects] = useState<Project[]>([])
|
const [projects, setProjects] = useState<ProjectData[]>([])
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
const { isLoaded: isAuthLoaded, userId } = useAuth()
|
const client = useGQLClient()
|
||||||
const { isLoaded: isUserLoaded, user } = useUser()
|
|
||||||
|
|
||||||
// Use the hook to fetch all repos (with an empty ID to get all)
|
const handleCreateProject = () => {
|
||||||
const { repoData: allRepos, isLoading: reposLoading, error: reposError } = useRepoData('');
|
window.location.href = '/projects/github/ps/cr'
|
||||||
|
|
||||||
const handleConnectGitHub = () => {
|
|
||||||
window.open('https://accounts.clerk.dev/user', '_blank');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Process repos data when it's loaded
|
loadAllProjects()
|
||||||
if (!reposLoading && allRepos) {
|
}, [])
|
||||||
// Transform GitHub repos to match ProjectData interface
|
|
||||||
const projectData: ProjectData[] = allRepos.map((repo: any) => ({
|
const loadAllProjects = async () => {
|
||||||
id: repo.id.toString(),
|
try {
|
||||||
name: repo.name,
|
setIsLoading(true)
|
||||||
full_name: repo.full_name,
|
setError(null)
|
||||||
// Create a deployment object that matches your existing structure
|
|
||||||
deployments: [
|
|
||||||
{
|
|
||||||
applicationDeploymentRecordData: {
|
|
||||||
url: repo.html_url
|
|
||||||
},
|
|
||||||
branch: repo.default_branch,
|
|
||||||
createdAt: repo.updated_at,
|
|
||||||
createdBy: {
|
|
||||||
name: repo.owner?.login || 'Unknown'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
setProjects(projectData);
|
// First get organizations
|
||||||
setIsLoading(false);
|
const orgsResponse = await client.getOrganizations()
|
||||||
} else if (!reposLoading && reposError) {
|
|
||||||
setError(reposError);
|
if (!orgsResponse.organizations || orgsResponse.organizations.length === 0) {
|
||||||
setIsLoading(false);
|
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)
|
||||||
|
} catch (orgError) {
|
||||||
|
console.error(`Failed to load projects for org ${org.slug}:`, orgError)
|
||||||
|
// Continue with other orgs even if one fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setProjects(allProjects)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load projects:', err)
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to load projects')
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}, [allRepos, reposLoading, reposError]);
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper
|
<PageWrapper
|
||||||
@ -92,16 +105,13 @@ export default function ProjectsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<h2 className="text-xl font-semibold mb-2">Error: {error}</h2>
|
<h2 className="text-xl font-semibold mb-2">Error: {error}</h2>
|
||||||
<p className="text-gray-400 text-center max-w-md mb-6">
|
<p className="text-gray-400 text-center max-w-md mb-6">
|
||||||
Please connect your GitHub account to see your repositories.
|
Failed to load your deployed projects. Please try again.
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
className="bg-white text-black hover:bg-gray-200 flex items-center"
|
className="bg-white text-black hover:bg-gray-200 flex items-center"
|
||||||
onClick={handleConnectGitHub}
|
onClick={loadAllProjects}
|
||||||
>
|
>
|
||||||
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
Try Again
|
||||||
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
|
|
||||||
</svg>
|
|
||||||
Connect to GitHub
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : projects.length === 0 ? (
|
) : projects.length === 0 ? (
|
||||||
@ -114,29 +124,77 @@ export default function ProjectsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<h2 className="text-xl font-semibold mb-2">Deploy your first app</h2>
|
<h2 className="text-xl font-semibold mb-2">Deploy your first app</h2>
|
||||||
<p className="text-gray-400 text-center max-w-md mb-6">
|
<p className="text-gray-400 text-center max-w-md mb-6">
|
||||||
Once connected, you can import a repository from your account or start with one of our templates.
|
You don't have any deployed projects yet. Create your first project to get started.
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
className="bg-white text-black hover:bg-gray-200 flex items-center"
|
className="bg-white text-black hover:bg-gray-200 flex items-center"
|
||||||
onClick={handleConnectGitHub}
|
onClick={handleCreateProject}
|
||||||
>
|
>
|
||||||
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
|
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||||
</svg>
|
</svg>
|
||||||
Connect to GitHub
|
Create Project
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// Custom grid that spans the entire bento layout
|
// Custom grid that spans the entire bento layout
|
||||||
<div className="md:col-span-3">
|
<div className="md:col-span-3">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{projects.map((project) => (
|
{projects.map((project) => {
|
||||||
<FixedProjectCard
|
// Get the current deployment for status
|
||||||
project={project as any}
|
const currentDeployment = project.deployments.find(d => d.isCurrent)
|
||||||
key={project.id}
|
const latestDeployment = project.deployments[0] // Assuming sorted by date
|
||||||
status={project.deployments[0]?.branch ? 'success' : 'pending'}
|
|
||||||
/>
|
// Determine status based on deployment
|
||||||
))}
|
let status = 'pending'
|
||||||
|
if (currentDeployment || latestDeployment) {
|
||||||
|
const deployment = currentDeployment || latestDeployment
|
||||||
|
switch (deployment.status?.toUpperCase()) {
|
||||||
|
case 'READY':
|
||||||
|
case 'COMPLETED':
|
||||||
|
status = 'success'
|
||||||
|
break
|
||||||
|
case 'BUILDING':
|
||||||
|
case 'DEPLOYING':
|
||||||
|
status = 'in-progress'
|
||||||
|
break
|
||||||
|
case 'ERROR':
|
||||||
|
case 'FAILED':
|
||||||
|
status = 'failure'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
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,
|
||||||
|
repository: project.repository,
|
||||||
|
framework: project.framework,
|
||||||
|
description: project.description,
|
||||||
|
// Ensure deployments array is properly formatted
|
||||||
|
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`
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FixedProjectCard
|
||||||
|
project={formattedProject}
|
||||||
|
key={project.id}
|
||||||
|
status={status as any}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
166
apps/deploy-fe/src/app/auth/github/backend-callback/page.tsx
Normal file
166
apps/deploy-fe/src/app/auth/github/backend-callback/page.tsx
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
// src/app/auth/github/backend-callback/page.tsx
|
||||||
|
'use client'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useSearchParams } from 'next/navigation'
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@workspace/ui/components/card'
|
||||||
|
import { CheckCircle2, AlertCircle, Loader2 } from 'lucide-react'
|
||||||
|
import { useGQLClient } from '@/context'
|
||||||
|
|
||||||
|
export default function GitHubBackendCallbackPage() {
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const [status, setStatus] = useState<'processing' | 'success' | 'error'>('processing')
|
||||||
|
const [message, setMessage] = useState('Processing GitHub authentication...')
|
||||||
|
const gqlClient = useGQLClient()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleCallback = async () => {
|
||||||
|
try {
|
||||||
|
// Get parameters from URL
|
||||||
|
const code = searchParams.get('code')
|
||||||
|
const state = searchParams.get('state')
|
||||||
|
const error = searchParams.get('error')
|
||||||
|
const errorDescription = searchParams.get('error_description')
|
||||||
|
|
||||||
|
// Check for OAuth errors
|
||||||
|
if (error) {
|
||||||
|
throw new Error(errorDescription || `GitHub OAuth error: ${error}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required parameters
|
||||||
|
if (!code) {
|
||||||
|
throw new Error('No authorization code received from GitHub')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify state parameter for security
|
||||||
|
const storedState = sessionStorage.getItem('github_oauth_state')
|
||||||
|
if (state !== storedState) {
|
||||||
|
throw new Error('Invalid state parameter - possible CSRF attack')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up stored state
|
||||||
|
sessionStorage.removeItem('github_oauth_state')
|
||||||
|
|
||||||
|
setMessage('Connecting to backend...')
|
||||||
|
|
||||||
|
// Call backend's authenticateGitHub mutation
|
||||||
|
const result = await gqlClient.authenticateGitHub(code)
|
||||||
|
|
||||||
|
if (result.authenticateGitHub?.token) {
|
||||||
|
setStatus('success')
|
||||||
|
setMessage('GitHub authentication successful!')
|
||||||
|
|
||||||
|
// Notify parent window
|
||||||
|
if (window.opener) {
|
||||||
|
window.opener.postMessage({
|
||||||
|
type: 'GITHUB_BACKEND_AUTH_SUCCESS',
|
||||||
|
token: result.authenticateGitHub.token
|
||||||
|
}, window.location.origin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close popup after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
window.close()
|
||||||
|
}, 2000)
|
||||||
|
} else {
|
||||||
|
throw new Error('No token received from backend')
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('GitHub OAuth callback error:', error)
|
||||||
|
setStatus('error')
|
||||||
|
setMessage(error instanceof Error ? error.message : 'Unknown error occurred')
|
||||||
|
|
||||||
|
// Notify parent window of error
|
||||||
|
if (window.opener) {
|
||||||
|
window.opener.postMessage({
|
||||||
|
type: 'GITHUB_BACKEND_AUTH_ERROR',
|
||||||
|
message: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
}, window.location.origin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close popup after delay even on error
|
||||||
|
setTimeout(() => {
|
||||||
|
window.close()
|
||||||
|
}, 5000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only run if we have search params (meaning this is the callback)
|
||||||
|
if (searchParams.toString()) {
|
||||||
|
handleCallback()
|
||||||
|
}
|
||||||
|
}, [searchParams, gqlClient])
|
||||||
|
|
||||||
|
const getStatusIcon = () => {
|
||||||
|
switch (status) {
|
||||||
|
case 'processing':
|
||||||
|
return <Loader2 className="h-8 w-8 animate-spin text-blue-600" />
|
||||||
|
case 'success':
|
||||||
|
return <CheckCircle2 className="h-8 w-8 text-green-600" />
|
||||||
|
case 'error':
|
||||||
|
return <AlertCircle className="h-8 w-8 text-red-600" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusColor = () => {
|
||||||
|
switch (status) {
|
||||||
|
case 'processing':
|
||||||
|
return 'text-blue-800'
|
||||||
|
case 'success':
|
||||||
|
return 'text-green-800'
|
||||||
|
case 'error':
|
||||||
|
return 'text-red-800'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 p-4">
|
||||||
|
<Card className="w-full max-w-md">
|
||||||
|
<CardHeader className="text-center">
|
||||||
|
<div className="flex justify-center mb-4">
|
||||||
|
{getStatusIcon()}
|
||||||
|
</div>
|
||||||
|
<CardTitle>GitHub Authentication</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{status === 'processing' && 'Processing your GitHub authentication...'}
|
||||||
|
{status === 'success' && 'Authentication completed successfully'}
|
||||||
|
{status === 'error' && 'Authentication failed'}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-center space-y-4">
|
||||||
|
<p className={`text-sm ${getStatusColor()}`}>
|
||||||
|
{message}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{status === 'success' && (
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
This window will close automatically...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status === 'error' && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
This window will close automatically in a few seconds.
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => window.close()}
|
||||||
|
className="text-sm text-blue-600 hover:text-blue-800 underline"
|
||||||
|
>
|
||||||
|
Close manually
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status === 'processing' && (
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
Please wait while we complete the authentication process...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import '@workspace/ui/globals.css'
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
import { Inter } from 'next/font/google'
|
import { Inter } from 'next/font/google'
|
||||||
import { CheckBalanceWrapper } from '@/components/iframe/check-balance-iframe/CheckBalanceWrapper'
|
import { CheckBalanceWrapper } from '@/components/iframe/check-balance-iframe/CheckBalanceWrapper'
|
||||||
|
import { AutoSignInIFrameModal } from '@/components/iframe/auto-sign-in'
|
||||||
|
|
||||||
// Add root metadata with template pattern
|
// Add root metadata with template pattern
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@ -29,6 +30,9 @@ export default function RootLayout({
|
|||||||
<body className={`${inter.className} `} suppressHydrationWarning>
|
<body className={`${inter.className} `} suppressHydrationWarning>
|
||||||
<main>
|
<main>
|
||||||
<Providers>{children}</Providers>
|
<Providers>{children}</Providers>
|
||||||
|
<div style={{ display: 'none' }}>
|
||||||
|
<AutoSignInIFrameModal />
|
||||||
|
</div>
|
||||||
<CheckBalanceWrapper />
|
<CheckBalanceWrapper />
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
882
apps/deploy-fe/src/app/test-connection/page.tsx
Normal file
882
apps/deploy-fe/src/app/test-connection/page.tsx
Normal file
@ -0,0 +1,882 @@
|
|||||||
|
// src/app/test-connection/page.tsx
|
||||||
|
'use client'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { PageWrapper } from '@/components/foundation'
|
||||||
|
import { DirectKeyAuth } from '@/components/DirectKeyAuth'
|
||||||
|
import { GQLTest } from '@/components/GQLTest'
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs'
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@workspace/ui/components/card'
|
||||||
|
import { useGQLClient } from '@/context'
|
||||||
|
import { Button } from '@workspace/ui/components/button'
|
||||||
|
import { Input } from '@workspace/ui/components/input'
|
||||||
|
import { Label } from '@workspace/ui/components/label'
|
||||||
|
import { Loader2, AlertTriangle, CheckCircle2, GitBranch } from 'lucide-react'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
import { useRepoData } from '@/hooks/useRepoData'
|
||||||
|
import { useAuth, useUser, SignIn } from "@clerk/nextjs"
|
||||||
|
import { GitHubBackendAuth } from '@/components/GitHubBackendAuth'
|
||||||
|
|
||||||
|
// Add this at the very top of your file
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
Clerk?: {
|
||||||
|
session?: {
|
||||||
|
getToken: (options?: { template?: string }) => Promise<string | null>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TestConnectionPage() {
|
||||||
|
// Get getToken from useAuth hook, not from user
|
||||||
|
const { isSignedIn, isLoaded: isClerkLoaded, getToken } = useAuth()
|
||||||
|
const { user, isLoaded: isUserLoaded } = useUser()
|
||||||
|
|
||||||
|
// Authentication states
|
||||||
|
const [isWalletConnected, setIsWalletConnected] = useState(false)
|
||||||
|
const [isBackendConnected, setIsBackendConnected] = useState(false)
|
||||||
|
const [isGithubAuthed, setIsGithubAuthed] = useState(false)
|
||||||
|
|
||||||
|
// Organization and deployment states
|
||||||
|
const [organizations, setOrganizations] = useState<any[]>([])
|
||||||
|
const [selectedOrg, setSelectedOrg] = useState<string>('')
|
||||||
|
const [isDeploying, setIsDeploying] = useState(false)
|
||||||
|
const [deploymentResult, setDeploymentResult] = useState<any>(null)
|
||||||
|
const [deploymentError, setDeploymentError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const [deployers, setDeployers] = useState<any[]>([])
|
||||||
|
const [selectedDeployer, setSelectedDeployer] = useState<string>('')
|
||||||
|
const [deployersLoading, setDeployersLoading] = useState(false)
|
||||||
|
|
||||||
|
// Form state
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: 'test-deployment',
|
||||||
|
repository: '',
|
||||||
|
branch: 'main',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Contexts and hooks
|
||||||
|
const gqlClient = useGQLClient()
|
||||||
|
|
||||||
|
// Use the useRepoData hook to get repositories (using Clerk's GitHub integration)
|
||||||
|
const { repoData: repositories } = useRepoData('')
|
||||||
|
|
||||||
|
// Check if both authentications are complete
|
||||||
|
const isFullyAuthenticated = isWalletConnected && isBackendConnected && isSignedIn && isGithubAuthed
|
||||||
|
// Add this near your other useState declarations at the top of the component
|
||||||
|
const [manualToken, setManualToken] = useState('')
|
||||||
|
|
||||||
|
// Update the function to use getToken from useAuth
|
||||||
|
const getClerkTokenForManualEntry = async () => {
|
||||||
|
if (!isSignedIn || !user) {
|
||||||
|
toast.error('Please sign in first')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Attempting to get token from useAuth...')
|
||||||
|
|
||||||
|
// Method 1: Try getToken from useAuth hook
|
||||||
|
let token = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
token = await getToken()
|
||||||
|
console.log('Method 1 (getToken from useAuth) worked:', token ? 'SUCCESS' : 'NO TOKEN')
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Method 1 failed:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 2: Try with 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 (as mentioned in discussions)
|
||||||
|
if (!token && typeof window !== 'undefined' && window.Clerk) {
|
||||||
|
try {
|
||||||
|
token = await window.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 template
|
||||||
|
if (!token && typeof window !== 'undefined' && window.Clerk) {
|
||||||
|
try {
|
||||||
|
token = await window.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) {
|
||||||
|
setManualToken(token)
|
||||||
|
// Copy to clipboard automatically
|
||||||
|
navigator.clipboard.writeText(token)
|
||||||
|
toast.success('Token extracted and copied to clipboard')
|
||||||
|
console.log('GitHub token from Clerk:', token.substring(0, 20) + '...')
|
||||||
|
} else {
|
||||||
|
toast.error('Unable to extract GitHub token. Check console for details.')
|
||||||
|
console.log('GitHub account object:', user.externalAccounts?.find(account => account.provider === 'github'))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting token from Clerk:', error)
|
||||||
|
toast.error(`Failed to get token: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check backend connection
|
||||||
|
const checkBackendConnection = async () => {
|
||||||
|
try {
|
||||||
|
// Test session
|
||||||
|
const response = await fetch('http://localhost:8000/auth/session', {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setIsBackendConnected(true)
|
||||||
|
console.log('Backend connected!')
|
||||||
|
|
||||||
|
// Check if user has GitHub token in backend
|
||||||
|
await checkBackendGithubAuth()
|
||||||
|
|
||||||
|
// Fetch organizations
|
||||||
|
try {
|
||||||
|
const orgsData = await gqlClient.getOrganizations()
|
||||||
|
console.log('Organizations:', orgsData)
|
||||||
|
setOrganizations(orgsData.organizations || [])
|
||||||
|
|
||||||
|
// Set default org if available
|
||||||
|
if (orgsData.organizations && orgsData.organizations.length > 0) {
|
||||||
|
setSelectedOrg(orgsData.organizations[0].slug)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching organizations:', error)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setIsBackendConnected(false)
|
||||||
|
console.log('Backend not connected')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking backend connection:', error)
|
||||||
|
setIsBackendConnected(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user has GitHub token in the backend database
|
||||||
|
const checkBackendGithubAuth = async () => {
|
||||||
|
try {
|
||||||
|
// Try to get user data from backend
|
||||||
|
const userData = await gqlClient.getUser()
|
||||||
|
console.log('Backend user data:', userData)
|
||||||
|
|
||||||
|
// Check if user has GitHub token in backend
|
||||||
|
setIsGithubAuthed(!!userData.user.gitHubToken)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking backend GitHub auth:', error)
|
||||||
|
setIsGithubAuthed(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync GitHub token from Clerk to backend
|
||||||
|
// Check wallet connection status whenever the backend connection changes
|
||||||
|
useEffect(() => {
|
||||||
|
const checkWalletConnection = async () => {
|
||||||
|
if (isBackendConnected) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://localhost:8000/auth/session', {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
|
||||||
|
setIsWalletConnected(response.ok)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking wallet connection:', error)
|
||||||
|
setIsWalletConnected(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkWalletConnection()
|
||||||
|
}, [isBackendConnected])
|
||||||
|
|
||||||
|
// Check backend connection on mount
|
||||||
|
useEffect(() => {
|
||||||
|
if (isClerkLoaded && isUserLoaded) {
|
||||||
|
checkBackendConnection()
|
||||||
|
}
|
||||||
|
}, [isClerkLoaded, isUserLoaded])
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||||
|
const { name, value } = e.target
|
||||||
|
setFormData(prev => ({ ...prev, [name]: value }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this function to fetch deployers:
|
||||||
|
const fetchDeployers = async () => {
|
||||||
|
try {
|
||||||
|
setDeployersLoading(true)
|
||||||
|
const deployersData = await gqlClient.getDeployers()
|
||||||
|
console.log('Available deployers:', deployersData)
|
||||||
|
setDeployers(deployersData.deployers || [])
|
||||||
|
|
||||||
|
// Auto-select first deployer if available
|
||||||
|
if (deployersData.deployers && deployersData.deployers.length > 0) {
|
||||||
|
setSelectedDeployer(deployersData.deployers[0].deployerLrn)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching deployers:', error)
|
||||||
|
toast.error('Failed to fetch deployers')
|
||||||
|
} finally {
|
||||||
|
setDeployersLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this useEffect to fetch deployers when backend is connected:
|
||||||
|
useEffect(() => {
|
||||||
|
if (isBackendConnected && isFullyAuthenticated) {
|
||||||
|
fetchDeployers()
|
||||||
|
}
|
||||||
|
}, [isBackendConnected, isFullyAuthenticated])
|
||||||
|
|
||||||
|
// Updated handleDeploy function:
|
||||||
|
const handleDeploy = async () => {
|
||||||
|
if (!isFullyAuthenticated) {
|
||||||
|
setDeploymentError('Complete authentication required. Please authenticate with both wallet and GitHub.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedOrg) {
|
||||||
|
setDeploymentError('No organization selected')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.repository) {
|
||||||
|
setDeploymentError('No repository selected')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedDeployer) {
|
||||||
|
setDeploymentError('No deployer selected')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsDeploying(true)
|
||||||
|
setDeploymentError(null)
|
||||||
|
setDeploymentResult(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('🚀 Starting deployment with data:', {
|
||||||
|
...formData,
|
||||||
|
organizationSlug: selectedOrg,
|
||||||
|
deployerLrn: selectedDeployer
|
||||||
|
})
|
||||||
|
|
||||||
|
// Validate repository format
|
||||||
|
if (!formData.repository.includes('/')) {
|
||||||
|
throw new Error('Repository must be in format "owner/repo-name"')
|
||||||
|
}
|
||||||
|
|
||||||
|
const [owner, repo] = formData.repository.split('/')
|
||||||
|
if (!owner || !repo) {
|
||||||
|
throw new Error('Invalid repository format. Expected "owner/repo-name"')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📤 Calling backend addProject mutation...')
|
||||||
|
|
||||||
|
// Use the addProject mutation with deployer LRN
|
||||||
|
const result = await gqlClient.addProject(
|
||||||
|
selectedOrg,
|
||||||
|
{
|
||||||
|
name: formData.name,
|
||||||
|
repository: formData.repository,
|
||||||
|
prodBranch: formData.branch,
|
||||||
|
paymentAddress: "0x1ac42F4A25Ae0137d10a825a2e33e32de0F6B57E",
|
||||||
|
txHash: "0x0000000000000000000000000000000000000000000000000000000000000000"
|
||||||
|
},
|
||||||
|
selectedDeployer, // Pass the deployer LRN here
|
||||||
|
undefined, // auctionParams
|
||||||
|
[] // environmentVariables
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log('Project creation result:', result)
|
||||||
|
|
||||||
|
if (result.addProject?.id) {
|
||||||
|
// Wait a moment to allow deployment to start
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||||
|
|
||||||
|
// Get updated project data with deployments
|
||||||
|
const projectData = await gqlClient.getProject(result.addProject.id)
|
||||||
|
console.log('Project data with deployments:', projectData)
|
||||||
|
|
||||||
|
setDeploymentResult({
|
||||||
|
project: projectData.project,
|
||||||
|
message: 'Project created successfully!'
|
||||||
|
})
|
||||||
|
|
||||||
|
toast.success('Project deployed successfully!')
|
||||||
|
} else {
|
||||||
|
throw new Error('No project ID returned from creation')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Deployment failed:', error)
|
||||||
|
|
||||||
|
let errorMessage = 'Unknown error'
|
||||||
|
if (error instanceof Error) {
|
||||||
|
errorMessage = error.message
|
||||||
|
}
|
||||||
|
|
||||||
|
setDeploymentError(`Failed to deploy: ${errorMessage}`)
|
||||||
|
toast.error('Deployment failed')
|
||||||
|
} finally {
|
||||||
|
setIsDeploying(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageWrapper
|
||||||
|
header={{
|
||||||
|
title: 'Connection & Deployment Test',
|
||||||
|
description: 'Test backend connection, authentication, and deployment functionality'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-1 gap-8 mt-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center">
|
||||||
|
Authentication Status
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className={`w-3 h-3 rounded-full mr-2 ${isWalletConnected ? 'bg-green-500' : 'bg-red-500'}`}></div>
|
||||||
|
<span>Wallet Connection: {isWalletConnected ? 'Connected' : 'Disconnected'}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className={`w-3 h-3 rounded-full mr-2 ${isBackendConnected ? 'bg-green-500' : 'bg-red-500'}`}></div>
|
||||||
|
<span>Backend Connection: {isBackendConnected ? 'Connected' : 'Disconnected'}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className={`w-3 h-3 rounded-full mr-2 ${isSignedIn ? 'bg-green-500' : 'bg-red-500'}`}></div>
|
||||||
|
<span>Clerk Authentication: {isSignedIn ? 'Signed In' : 'Not Signed In'}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className={`w-3 h-3 rounded-full mr-2 ${isGithubAuthed ? 'bg-green-500' : 'bg-red-500'}`}></div>
|
||||||
|
<span>GitHub (Backend): {isGithubAuthed ? 'Authenticated' : 'Not Authenticated'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<div className={`p-3 rounded-md ${isFullyAuthenticated ? 'bg-green-100 text-green-800' : 'bg-amber-100 text-amber-800'}`}>
|
||||||
|
<div className="flex items-center">
|
||||||
|
{isFullyAuthenticated ? (
|
||||||
|
<CheckCircle2 className="h-5 w-5 mr-2" />
|
||||||
|
) : (
|
||||||
|
<AlertTriangle className="h-5 w-5 mr-2" />
|
||||||
|
)}
|
||||||
|
<span className="font-medium">
|
||||||
|
{isFullyAuthenticated
|
||||||
|
? 'All authentication requirements met - Ready to deploy!'
|
||||||
|
: 'Complete all authentication steps to enable deployment'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={checkBackendConnection}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="mt-4"
|
||||||
|
>
|
||||||
|
Refresh Status
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Tabs defaultValue="wallet">
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="wallet">Wallet Auth</TabsTrigger>
|
||||||
|
<TabsTrigger value="clerk">Clerk Auth</TabsTrigger>
|
||||||
|
<TabsTrigger value="github">GitHub Sync</TabsTrigger>
|
||||||
|
<TabsTrigger value="gql">GraphQL</TabsTrigger>
|
||||||
|
<TabsTrigger value="deploy">Deployment</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="wallet">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Wallet Authentication</h2>
|
||||||
|
<p className="text-sm text-gray-600 mb-4">
|
||||||
|
This authenticates your wallet with the backend for payment processing and transaction signing.
|
||||||
|
</p>
|
||||||
|
<DirectKeyAuth />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="clerk">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Clerk Authentication</h2>
|
||||||
|
<p className="text-sm text-gray-600 mb-4">
|
||||||
|
This provides GitHub authentication and user management through Clerk.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{!isSignedIn ? (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Sign In with Clerk</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Sign in to access GitHub repositories and user management features
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<SignIn />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Clerk Authentication Status</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
You are signed in with Clerk
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="p-3 bg-green-100 text-green-800 rounded flex items-center mb-4">
|
||||||
|
<CheckCircle2 className="h-5 w-5 mr-2" />
|
||||||
|
<span>Successfully signed in with Clerk</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p><strong>User:</strong> {user?.emailAddresses[0]?.emailAddress}</p>
|
||||||
|
<p><strong>User ID:</strong> {user?.id}</p>
|
||||||
|
<p><strong>GitHub Connected:</strong> {
|
||||||
|
user?.externalAccounts.find(account => account.provider === 'github')
|
||||||
|
? 'Yes' : 'No'
|
||||||
|
}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!user?.externalAccounts.find(account => account.provider === 'github') && (
|
||||||
|
<div className="mt-4 p-3 bg-amber-100 text-amber-800 rounded">
|
||||||
|
<p className="text-sm">
|
||||||
|
You need to connect your GitHub account in Clerk to proceed.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
onClick={() => window.open('https://accounts.clerk.dev/user', '_blank')}
|
||||||
|
className="mt-2"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Connect GitHub Account
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="github">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">GitHub Authentication</h2>
|
||||||
|
<p className="text-sm text-gray-600 mb-4">
|
||||||
|
This page manages two separate GitHub connections for different purposes.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Clerk GitHub Integration */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Clerk GitHub Integration</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Provides repository access and user management through Clerk
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className={`w-3 h-3 rounded-full mr-2 ${isSignedIn ? 'bg-green-500' : 'bg-red-500'}`}></div>
|
||||||
|
<span>Clerk Authentication: {isSignedIn ? 'Signed In' : 'Not Signed In'}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className={`w-3 h-3 rounded-full mr-2 ${
|
||||||
|
isSignedIn && user?.externalAccounts.find(account => account.provider === 'github')
|
||||||
|
? 'bg-green-500' : 'bg-red-500'
|
||||||
|
}`}></div>
|
||||||
|
<span>GitHub Connected to Clerk: {
|
||||||
|
isSignedIn && user?.externalAccounts.find(account => account.provider === 'github')
|
||||||
|
? 'Yes' : 'No'
|
||||||
|
}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{repositories && repositories.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-md font-semibold mb-2">Available Repositories (via Clerk)</h3>
|
||||||
|
<div className="border rounded-md max-h-40 overflow-y-auto">
|
||||||
|
<ul className="divide-y">
|
||||||
|
{repositories.slice(0, 5).map((repo: any) => (
|
||||||
|
<li key={repo.id} className="p-2 text-sm">
|
||||||
|
<span className="font-medium">{repo.full_name}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
{repositories.length > 5 && (
|
||||||
|
<li className="p-2 text-sm text-gray-500">
|
||||||
|
... and {repositories.length - 5} more repositories
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Token extraction for debugging */}
|
||||||
|
<div className="border-t pt-4">
|
||||||
|
<h3 className="text-md font-semibold mb-2">Debug: Token Extraction</h3>
|
||||||
|
<Button
|
||||||
|
onClick={getClerkTokenForManualEntry}
|
||||||
|
disabled={!isSignedIn || !user?.externalAccounts.find(account => account.provider === 'github')}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<GitBranch className="mr-2 h-4 w-4" />
|
||||||
|
Extract Clerk GitHub Token
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{manualToken && (
|
||||||
|
<div className="mt-3 space-y-2">
|
||||||
|
<div className="p-2 bg-gray-100 dark:bg-gray-800 rounded font-mono text-xs break-all">
|
||||||
|
{manualToken.substring(0, 40)}...
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-600">
|
||||||
|
Token extracted successfully (showing first 40 characters)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Backend GitHub Authentication */}
|
||||||
|
<GitHubBackendAuth
|
||||||
|
onAuthStatusChange={(isAuth) => setIsGithubAuthed(isAuth)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Status Summary */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Authentication Summary</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Overview of all authentication systems
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 gap-3">
|
||||||
|
<div className="flex items-center justify-between p-3 border rounded">
|
||||||
|
<span className="font-medium">Clerk GitHub (Repository Access)</span>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className={`w-3 h-3 rounded-full mr-2 ${
|
||||||
|
isSignedIn && user?.externalAccounts.find(account => account.provider === 'github')
|
||||||
|
? 'bg-green-500' : 'bg-red-500'
|
||||||
|
}`}></div>
|
||||||
|
<span className="text-sm">
|
||||||
|
{isSignedIn && user?.externalAccounts.find(account => account.provider === 'github')
|
||||||
|
? 'Connected' : 'Not Connected'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between p-3 border rounded">
|
||||||
|
<span className="font-medium">Backend GitHub (Deployments)</span>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className={`w-3 h-3 rounded-full mr-2 ${isGithubAuthed ? 'bg-green-500' : 'bg-red-500'}`}></div>
|
||||||
|
<span className="text-sm">{isGithubAuthed ? 'Connected' : 'Not Connected'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between p-3 border rounded">
|
||||||
|
<span className="font-medium">Wallet Authentication</span>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className={`w-3 h-3 rounded-full mr-2 ${isWalletConnected ? 'bg-green-500' : 'bg-red-500'}`}></div>
|
||||||
|
<span className="text-sm">{isWalletConnected ? 'Connected' : 'Not Connected'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={`mt-4 p-3 rounded-md ${isFullyAuthenticated ? 'bg-green-100 text-green-800' : 'bg-amber-100 text-amber-800'}`}>
|
||||||
|
<div className="flex items-center">
|
||||||
|
{isFullyAuthenticated ? (
|
||||||
|
<CheckCircle2 className="h-5 w-5 mr-2" />
|
||||||
|
) : (
|
||||||
|
<AlertTriangle className="h-5 w-5 mr-2" />
|
||||||
|
)}
|
||||||
|
<span className="font-medium">
|
||||||
|
{isFullyAuthenticated
|
||||||
|
? 'All systems connected - Ready for deployment!'
|
||||||
|
: 'Complete all authentication steps to enable deployment'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="gql">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">GraphQL Testing</h2>
|
||||||
|
<GQLTest />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="deploy">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Deployment Testing</h2>
|
||||||
|
|
||||||
|
{!isFullyAuthenticated ? (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Complete Authentication Required</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
You need to complete all authentication steps before deploying
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 gap-2">
|
||||||
|
<div className="p-3 border rounded flex items-center">
|
||||||
|
<div className={`w-3 h-3 rounded-full mr-2 ${isWalletConnected ? 'bg-green-500' : 'bg-red-500'}`}></div>
|
||||||
|
<span>Wallet Authentication: {isWalletConnected ? 'Complete' : 'Required'}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 border rounded flex items-center">
|
||||||
|
<div className={`w-3 h-3 rounded-full mr-2 ${isSignedIn ? 'bg-green-500' : 'bg-red-500'}`}></div>
|
||||||
|
<span>Clerk Authentication: {isSignedIn ? 'Complete' : 'Required'}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 border rounded flex items-center">
|
||||||
|
<div className={`w-3 h-3 rounded-full mr-2 ${isGithubAuthed ? 'bg-green-500' : 'bg-red-500'}`}></div>
|
||||||
|
<span>GitHub Backend Sync: {isGithubAuthed ? 'Complete' : 'Required'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-blue-50 rounded-md">
|
||||||
|
<h3 className="text-sm font-medium text-blue-800 mb-2">Next Steps:</h3>
|
||||||
|
<ol className="list-decimal pl-4 text-sm text-blue-700 space-y-1">
|
||||||
|
{!isWalletConnected && <li>Complete wallet authentication in the Wallet Auth tab</li>}
|
||||||
|
{!isSignedIn && <li>Sign in with Clerk in the Clerk Auth tab</li>}
|
||||||
|
{!isGithubAuthed && <li>Sync GitHub token in the GitHub Sync tab</li>}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Test Deployment</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Deploy a test project to verify deployment functionality
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{organizations.length > 0 ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="organization">Organization</Label>
|
||||||
|
<select
|
||||||
|
id="organization"
|
||||||
|
className="w-full p-2 border rounded"
|
||||||
|
value={selectedOrg}
|
||||||
|
onChange={(e) => setSelectedOrg(e.target.value)}
|
||||||
|
>
|
||||||
|
{organizations.map(org => (
|
||||||
|
<option key={org.id} value={org.slug}>
|
||||||
|
{org.name} ({org.slug})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="p-3 bg-amber-100 text-amber-800 rounded">
|
||||||
|
No organizations found. You need to be part of at least one organization.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Deployer Selection */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="deployer">Deployer</Label>
|
||||||
|
{deployersLoading ? (
|
||||||
|
<div className="p-2 border rounded bg-gray-50">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-900 mr-2"></div>
|
||||||
|
Loading deployers...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : deployers.length > 0 ? (
|
||||||
|
<select
|
||||||
|
id="deployer"
|
||||||
|
className="w-full p-2 border rounded"
|
||||||
|
value={selectedDeployer}
|
||||||
|
onChange={(e) => setSelectedDeployer(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">Select a deployer</option>
|
||||||
|
{deployers.map((deployer) => (
|
||||||
|
<option key={deployer.deployerLrn} value={deployer.deployerLrn}>
|
||||||
|
{deployer.deployerLrn}
|
||||||
|
{deployer.minimumPayment && ` (Min: ${deployer.minimumPayment})`}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
) : (
|
||||||
|
<div className="p-3 bg-amber-100 text-amber-800 rounded">
|
||||||
|
<p className="text-sm">
|
||||||
|
No deployers available. The backend needs to have deployers configured.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
onClick={fetchDeployers}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="mt-2"
|
||||||
|
>
|
||||||
|
Refresh Deployers
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="name">Project Name</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="test-deployment"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="repository">Repository</Label>
|
||||||
|
{repositories && repositories.length > 0 ? (
|
||||||
|
<select
|
||||||
|
id="repository"
|
||||||
|
name="repository"
|
||||||
|
className="w-full p-2 border rounded"
|
||||||
|
value={formData.repository}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
<option value="">Select a repository</option>
|
||||||
|
{repositories.map((repo: any) => (
|
||||||
|
<option key={repo.id} value={repo.full_name}>
|
||||||
|
{repo.full_name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<p className="mb-2 text-sm text-amber-800">
|
||||||
|
Enter the repository manually (format: owner/repo-name)
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
|
id="repository"
|
||||||
|
name="repository"
|
||||||
|
value={formData.repository}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="username/repo-name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="branch">Branch</Label>
|
||||||
|
<Input
|
||||||
|
id="branch"
|
||||||
|
name="branch"
|
||||||
|
value={formData.branch}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="main or master"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleDeploy}
|
||||||
|
disabled={isDeploying || !selectedOrg || !formData.repository || !selectedDeployer}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{isDeploying ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Deploying...
|
||||||
|
</>
|
||||||
|
) : 'Deploy Test Project'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{deploymentError && (
|
||||||
|
<div className="p-3 bg-red-100 text-red-800 rounded">
|
||||||
|
{deploymentError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{deploymentResult && (
|
||||||
|
<div className="p-3 bg-green-100 text-green-800 rounded">
|
||||||
|
<div className="flex items-start">
|
||||||
|
<CheckCircle2 className="h-5 w-5 mr-2 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium">{deploymentResult.message}</h3>
|
||||||
|
<p className="text-sm mt-1">Project ID: {deploymentResult.project?.id}</p>
|
||||||
|
<p className="text-sm">Name: {deploymentResult.project?.name}</p>
|
||||||
|
<p className="text-sm">Repository: {deploymentResult.project?.repository}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<details className="mt-2">
|
||||||
|
<summary className="cursor-pointer text-sm font-medium">
|
||||||
|
Show full project details
|
||||||
|
</summary>
|
||||||
|
<pre className="bg-white p-2 rounded mt-1 overflow-auto max-h-64 text-xs">
|
||||||
|
{JSON.stringify(deploymentResult.project, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 p-4 bg-gray-100 dark:bg-gray-800 rounded-md">
|
||||||
|
<h2 className="text-lg font-semibold mb-2">Hybrid Authentication Flow</h2>
|
||||||
|
<p className="mb-2 text-sm">
|
||||||
|
This deployment system requires both wallet and GitHub authentication:
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-md font-semibold mb-2">Wallet Authentication (DirectKeyAuth)</h3>
|
||||||
|
<ul className="list-disc pl-5 space-y-1 text-sm">
|
||||||
|
<li>Provides Ethereum wallet connection</li>
|
||||||
|
<li>Enables transaction signing for payments</li>
|
||||||
|
<li>Required for deployment costs and blockchain operations</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-md font-semibold mb-2">GitHub Authentication (Clerk)</h3>
|
||||||
|
<ul className="list-disc pl-5 space-y-1 text-sm">
|
||||||
|
<li>Provides access to GitHub repositories</li>
|
||||||
|
<li>Enables repository cloning and deployment</li>
|
||||||
|
<li>Required for backend deployment operations</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
411
apps/deploy-fe/src/components/AuthTest.tsx
Normal file
411
apps/deploy-fe/src/components/AuthTest.tsx
Normal file
@ -0,0 +1,411 @@
|
|||||||
|
// src/components/SIWEAuth.tsx with raw signature approach
|
||||||
|
'use client'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useWallet } from '@/context/WalletContext'
|
||||||
|
import { Button } from '@workspace/ui/components/button'
|
||||||
|
import { CheckBalanceWrapper } from './iframe/check-balance-iframe/CheckBalanceWrapper'
|
||||||
|
import { CopyIcon } from 'lucide-react'
|
||||||
|
|
||||||
|
// Generate a random nonce
|
||||||
|
function generateNonce() {
|
||||||
|
return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SIWEAuth() {
|
||||||
|
const { wallet, isConnected, connect, disconnect } = useWallet()
|
||||||
|
const [sessionStatus, setSessionStatus] = useState<'checking' | 'authenticated' | 'unauthenticated'>('checking')
|
||||||
|
const [sessionData, setSessionData] = useState<any>(null)
|
||||||
|
const [isAuthenticating, setIsAuthenticating] = useState(false)
|
||||||
|
const [authError, setAuthError] = useState<string | null>(null)
|
||||||
|
const [signedMessage, setSignedMessage] = useState<string | null>(null)
|
||||||
|
const [messageToSign, setMessageToSign] = useState<string | null>(null)
|
||||||
|
const [debugInfo, setDebugInfo] = useState<string>('')
|
||||||
|
|
||||||
|
// Check if we already have a session
|
||||||
|
const checkSession = async () => {
|
||||||
|
try {
|
||||||
|
setSessionStatus('checking')
|
||||||
|
const response = await fetch('http://localhost:8000/auth/session', {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
setSessionStatus('authenticated')
|
||||||
|
setSessionData(data)
|
||||||
|
console.log('Session check successful:', data)
|
||||||
|
} else {
|
||||||
|
setSessionStatus('unauthenticated')
|
||||||
|
setSessionData(null)
|
||||||
|
console.log('Session check failed:', await response.text())
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking session:', error)
|
||||||
|
setSessionStatus('unauthenticated')
|
||||||
|
setSessionData(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check session on component mount
|
||||||
|
useEffect(() => {
|
||||||
|
checkSession()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Copy text to clipboard
|
||||||
|
const copyToClipboard = (text: string) => {
|
||||||
|
navigator.clipboard.writeText(text)
|
||||||
|
.then(() => {
|
||||||
|
console.log('Text copied to clipboard')
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Could not copy text: ', err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a SIWE message with the correct Ethereum address
|
||||||
|
const createSiweMessage = () => {
|
||||||
|
// We want to try both our displayed address and the expected one from errors
|
||||||
|
// We'll use the displayed address by default
|
||||||
|
const ethAddress = '0x1ac42F4A25Ae0137d10a825a2e33e32de0F6B57E';
|
||||||
|
|
||||||
|
const domain = window.location.host
|
||||||
|
const origin = window.location.origin
|
||||||
|
const chainId = 1 // Ethereum mainnet
|
||||||
|
const statement = 'Sign in With Ethereum.'
|
||||||
|
const nonce = generateNonce()
|
||||||
|
const issuedAt = new Date().toISOString()
|
||||||
|
|
||||||
|
// IMPORTANT: This format must exactly match what the SiweMessage constructor expects
|
||||||
|
return `${domain} wants you to sign in with your Ethereum account:
|
||||||
|
${ethAddress}
|
||||||
|
|
||||||
|
${statement}
|
||||||
|
|
||||||
|
URI: ${origin}
|
||||||
|
Version: 1
|
||||||
|
Chain ID: ${chainId}
|
||||||
|
Nonce: ${nonce}
|
||||||
|
Issued At: ${issuedAt}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the message for signing
|
||||||
|
const generateMessageToSign = async () => {
|
||||||
|
if (!wallet?.address) {
|
||||||
|
setAuthError('Wallet not connected')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsAuthenticating(true)
|
||||||
|
setAuthError(null)
|
||||||
|
|
||||||
|
// Create a SIWE message with the Ethereum address
|
||||||
|
const message = createSiweMessage()
|
||||||
|
console.log('SIWE Message with Ethereum address:', message)
|
||||||
|
setDebugInfo(`Generated message with Ethereum address. IMPORTANT: Make sure "Ethereum" is selected in the wallet dropdown when signing.`)
|
||||||
|
|
||||||
|
// Set the message to sign
|
||||||
|
setMessageToSign(message)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating message:', error)
|
||||||
|
setAuthError(`Error: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
} finally {
|
||||||
|
setIsAuthenticating(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check auth without sending signature
|
||||||
|
const checkAuthWithoutSignature = async () => {
|
||||||
|
try {
|
||||||
|
setIsAuthenticating(true)
|
||||||
|
setAuthError(null)
|
||||||
|
setDebugInfo('Trying auth without signature...')
|
||||||
|
|
||||||
|
// Create API route to handle this
|
||||||
|
const response = await fetch('/api/dev-auth', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
address: '0x1ac42F4A25Ae0137d10a825a2e33e32de0F6B57E'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
console.log('Dev auth response:', data)
|
||||||
|
setDebugInfo(prev => `${prev}\nDev auth response: ${JSON.stringify(data)}`)
|
||||||
|
|
||||||
|
if (response.ok && data.success) {
|
||||||
|
console.log('Dev auth successful!')
|
||||||
|
setDebugInfo(prev => `${prev}\nDev auth successful!`)
|
||||||
|
await checkSession()
|
||||||
|
} else {
|
||||||
|
throw new Error(`Dev auth failed: ${JSON.stringify(data)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Dev auth error:', error)
|
||||||
|
setAuthError(`Error: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
} finally {
|
||||||
|
setIsAuthenticating(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit signature to validate
|
||||||
|
const submitSignature = async () => {
|
||||||
|
if (!messageToSign || !signedMessage) {
|
||||||
|
setAuthError('Missing message or signature')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsAuthenticating(true)
|
||||||
|
setAuthError(null)
|
||||||
|
setDebugInfo(prev => `${prev}\nSubmitting raw signature...`)
|
||||||
|
|
||||||
|
// Log the original signature
|
||||||
|
console.log('Raw signature:', signedMessage)
|
||||||
|
setDebugInfo(prev => `${prev}\nRaw signature: ${signedMessage}`)
|
||||||
|
|
||||||
|
// Try using the raw signature directly
|
||||||
|
const response = await fetch('http://localhost:8000/auth/validate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: messageToSign,
|
||||||
|
signature: signedMessage
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
let responseData = {}
|
||||||
|
try {
|
||||||
|
responseData = await response.json()
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Error parsing response:', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Validation response:', responseData)
|
||||||
|
setDebugInfo(prev => `${prev}\nValidation response: ${JSON.stringify(responseData)}`)
|
||||||
|
|
||||||
|
// If successful, we're done
|
||||||
|
if (response.ok && responseData.success) {
|
||||||
|
console.log('Authentication successful!')
|
||||||
|
setDebugInfo(prev => `${prev}\nAuthentication successful!`)
|
||||||
|
|
||||||
|
// Clear message and signature
|
||||||
|
setMessageToSign(null)
|
||||||
|
setSignedMessage(null)
|
||||||
|
|
||||||
|
// Check if we now have a session
|
||||||
|
await checkSession()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get here, it failed
|
||||||
|
throw new Error(`Validation failed: ${JSON.stringify(responseData)}`)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Authentication error:', error)
|
||||||
|
setAuthError(`Error: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
setSessionStatus('unauthenticated')
|
||||||
|
} finally {
|
||||||
|
setIsAuthenticating(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 border rounded-md">
|
||||||
|
{/* Hidden iframe for wallet connection */}
|
||||||
|
<CheckBalanceWrapper />
|
||||||
|
|
||||||
|
<h2 className="text-lg font-bold mb-4">Sign-In With Ethereum</h2>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-md font-semibold mb-2">Wallet Status</h3>
|
||||||
|
<p className="mb-2">
|
||||||
|
Status: <span className={isConnected ? "text-green-500" : "text-red-500"}>
|
||||||
|
{isConnected ? 'Connected' : 'Disconnected'}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
{wallet && wallet.address && (
|
||||||
|
<div className="p-2 bg-gray-800 text-white rounded mb-2">
|
||||||
|
<p className="font-mono text-sm break-all">Laconic Address: {wallet.address}</p>
|
||||||
|
<p className="font-mono text-sm break-all mt-1">Ethereum Address: 0x1ac42F4A25Ae0137d10a825a2e33e32de0F6B57E</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-2">
|
||||||
|
{!isConnected ? (
|
||||||
|
<Button onClick={connect}>Connect Wallet</Button>
|
||||||
|
) : (
|
||||||
|
<Button variant="outline" onClick={disconnect}>Disconnect</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isConnected && sessionStatus !== 'authenticated' && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-md font-semibold mb-2">Authentication</h3>
|
||||||
|
|
||||||
|
<div className="p-3 bg-amber-100 text-amber-800 rounded mb-4">
|
||||||
|
<p className="font-semibold text-sm">IMPORTANT:</p>
|
||||||
|
<p className="text-sm">When signing the message, make sure "Ethereum" is selected in the wallet's network dropdown.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!messageToSign ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Button
|
||||||
|
onClick={generateMessageToSign}
|
||||||
|
disabled={isAuthenticating}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
Generate SIWE Message
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="border-t pt-4">
|
||||||
|
<p className="text-sm mb-2 text-amber-700 font-semibold">
|
||||||
|
Alternative Authentication Methods
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
onClick={checkAuthWithoutSignature}
|
||||||
|
disabled={isAuthenticating}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
Try Development Authentication
|
||||||
|
</Button>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
This will try to create a session using a development-only endpoint.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="p-3 bg-gray-800 text-white rounded mb-3">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h4 className="text-sm font-semibold">Message to Sign:</h4>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => copyToClipboard(messageToSign)}
|
||||||
|
className="h-8 px-2"
|
||||||
|
>
|
||||||
|
<CopyIcon className="h-4 w-4 mr-1" /> Copy
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<pre className="font-mono text-xs whitespace-pre-wrap break-all">{messageToSign}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<p className="text-sm mb-2">
|
||||||
|
1. Copy the message above
|
||||||
|
</p>
|
||||||
|
<p className="text-sm mb-2">
|
||||||
|
2. Go to your wallet's "Sign Message" page
|
||||||
|
(<a href="http://localhost:4000/SignMessage" target="_blank" className="text-blue-500 underline">
|
||||||
|
Open Wallet Sign Page
|
||||||
|
</a>)
|
||||||
|
</p>
|
||||||
|
<p className="text-sm mb-2 font-medium text-amber-700">
|
||||||
|
3. Make sure "Ethereum" is selected in the network dropdown
|
||||||
|
</p>
|
||||||
|
<p className="text-sm mb-2">
|
||||||
|
4. Paste the message and sign it
|
||||||
|
</p>
|
||||||
|
<p className="text-sm mb-2">
|
||||||
|
5. Copy the ENTIRE signature and paste it below
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<h4 className="text-sm font-semibold mb-2">Paste Signature:</h4>
|
||||||
|
<textarea
|
||||||
|
className="w-full p-2 border rounded dark:bg-gray-800 dark:text-white"
|
||||||
|
rows={3}
|
||||||
|
value={signedMessage || ''}
|
||||||
|
onChange={(e) => setSignedMessage(e.target.value)}
|
||||||
|
placeholder="Paste signature here (including 'Signature' prefix)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={submitSignature}
|
||||||
|
disabled={isAuthenticating || !signedMessage}
|
||||||
|
className="mb-2"
|
||||||
|
>
|
||||||
|
{isAuthenticating ? 'Validating...' : 'Validate Signature'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setMessageToSign(null);
|
||||||
|
setSignedMessage(null);
|
||||||
|
setDebugInfo('');
|
||||||
|
}}
|
||||||
|
className="ml-2 mb-2"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{debugInfo && (
|
||||||
|
<div className="mt-4 p-2 bg-gray-800 text-white rounded">
|
||||||
|
<h4 className="text-sm font-semibold mb-2">Debug Information:</h4>
|
||||||
|
<pre className="font-mono text-xs whitespace-pre-wrap">{debugInfo}</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{authError && (
|
||||||
|
<div className="mt-2 p-2 bg-red-100 border border-red-300 text-red-800 rounded whitespace-pre-line">
|
||||||
|
{authError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-md font-semibold mb-2">Backend Session</h3>
|
||||||
|
<p className="mb-2">
|
||||||
|
Status:
|
||||||
|
<span className={
|
||||||
|
sessionStatus === 'authenticated' ? "text-green-500" :
|
||||||
|
sessionStatus === 'unauthenticated' ? "text-red-500" :
|
||||||
|
"text-yellow-500"
|
||||||
|
}>
|
||||||
|
{' '}{sessionStatus}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{sessionData && (
|
||||||
|
<div className="p-2 bg-gray-800 text-white rounded mb-2">
|
||||||
|
<pre className="font-mono text-sm overflow-auto max-h-32">{JSON.stringify(sessionData, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-2">
|
||||||
|
<Button variant="outline" onClick={checkSession}>Check Session</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 p-3 bg-blue-50 text-blue-800 rounded text-sm">
|
||||||
|
<p className="font-semibold mb-1">About Laconic Wallet Authentication:</p>
|
||||||
|
<p className="mt-2 text-xs">
|
||||||
|
The Laconic wallet supports multiple networks including Ethereum. For SIWE authentication, you must:
|
||||||
|
</p>
|
||||||
|
<ol className="list-decimal text-xs mt-1 pl-4">
|
||||||
|
<li>Use your Ethereum address in the sign-in message</li>
|
||||||
|
<li>Make sure "Ethereum" is selected in the network dropdown when signing</li>
|
||||||
|
<li>The signature will then be created with your Ethereum private key</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
171
apps/deploy-fe/src/components/DeploymentTest.tsx
Normal file
171
apps/deploy-fe/src/components/DeploymentTest.tsx
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
'use client'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Button } from '@workspace/ui/components/button'
|
||||||
|
import { Input } from '@workspace/ui/components/input'
|
||||||
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@workspace/ui/components/card'
|
||||||
|
import { Label } from '@workspace/ui/components/label'
|
||||||
|
import { useDeployment, type DeploymentConfig } from '@/hooks/useDeployment'
|
||||||
|
import { Loader2 } from 'lucide-react'
|
||||||
|
|
||||||
|
interface DeploymentFormProps {
|
||||||
|
organizationSlug: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeploymentForm({ organizationSlug }: DeploymentFormProps) {
|
||||||
|
const { deployRepository, isDeploying, deploymentResult } = useDeployment()
|
||||||
|
const [formData, setFormData] = useState<Omit<DeploymentConfig, 'organizationSlug'>>({
|
||||||
|
projectId: '',
|
||||||
|
repository: '',
|
||||||
|
branch: 'main',
|
||||||
|
name: '',
|
||||||
|
environmentVariables: []
|
||||||
|
})
|
||||||
|
const [envVars, setEnvVars] = useState<Array<{ key: string; value: string }>>([])
|
||||||
|
const [currentEnvVar, setCurrentEnvVar] = useState({ key: '', value: '' })
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name, value } = e.target
|
||||||
|
setFormData(prev => ({ ...prev, [name]: value }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddEnvVar = () => {
|
||||||
|
if (currentEnvVar.key && currentEnvVar.value) {
|
||||||
|
setEnvVars(prev => [...prev, { ...currentEnvVar }])
|
||||||
|
setCurrentEnvVar({ key: '', value: '' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeploy = async () => {
|
||||||
|
try {
|
||||||
|
// Convert the env vars to the format expected by the API
|
||||||
|
const environmentVariables = envVars.map(ev => ({
|
||||||
|
key: ev.key,
|
||||||
|
value: ev.value,
|
||||||
|
environments: ['Production', 'Preview'] // Default to both environments
|
||||||
|
}))
|
||||||
|
|
||||||
|
await deployRepository({
|
||||||
|
...formData,
|
||||||
|
organizationSlug,
|
||||||
|
environmentVariables
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Deployment failed:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Deploy Repository</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Enter the details for deploying a GitHub repository
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="name">Project Name</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="my-awesome-project"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="repository">Repository URL</Label>
|
||||||
|
<Input
|
||||||
|
id="repository"
|
||||||
|
name="repository"
|
||||||
|
value={formData.repository}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="https://github.com/username/repo"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="branch">Branch</Label>
|
||||||
|
<Input
|
||||||
|
id="branch"
|
||||||
|
name="branch"
|
||||||
|
value={formData.branch}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="main"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Environment Variables</Label>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="KEY"
|
||||||
|
value={currentEnvVar.key}
|
||||||
|
onChange={(e) => setCurrentEnvVar(prev => ({ ...prev, key: e.target.value }))}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="value"
|
||||||
|
value={currentEnvVar.value}
|
||||||
|
onChange={(e) => setCurrentEnvVar(prev => ({ ...prev, value: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
type="button"
|
||||||
|
onClick={handleAddEnvVar}
|
||||||
|
disabled={!currentEnvVar.key || !currentEnvVar.value}
|
||||||
|
className="mt-2"
|
||||||
|
>
|
||||||
|
Add Environment Variable
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{envVars.length > 0 && (
|
||||||
|
<div className="border rounded p-2">
|
||||||
|
<h4 className="font-medium mb-2">Environment Variables:</h4>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{envVars.map((ev, index) => (
|
||||||
|
<li key={index} className="flex justify-between">
|
||||||
|
<span className="font-mono">{ev.key}</span>
|
||||||
|
<span className="font-mono text-gray-500">{ev.value}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button
|
||||||
|
onClick={handleDeploy}
|
||||||
|
disabled={isDeploying || !formData.name || !formData.repository}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{isDeploying ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Deploying...
|
||||||
|
</>
|
||||||
|
) : 'Deploy Repository'}
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
|
||||||
|
{deploymentResult && (
|
||||||
|
<div className="mt-4 p-4 border-t">
|
||||||
|
<h3 className="font-medium mb-2">Deployment Result:</h3>
|
||||||
|
<p>Status: <span className="font-medium">{deploymentResult.status}</span></p>
|
||||||
|
{deploymentResult.url && (
|
||||||
|
<p className="mt-2">
|
||||||
|
URL: <a href={deploymentResult.url} target="_blank" rel="noopener noreferrer" className="text-blue-500 underline">{deploymentResult.url}</a>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
160
apps/deploy-fe/src/components/DirectKeyAuth.tsx
Normal file
160
apps/deploy-fe/src/components/DirectKeyAuth.tsx
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
// src/components/DirectKeyAuth.tsx
|
||||||
|
'use client'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { Button } from '@workspace/ui/components/button'
|
||||||
|
import { Wallet } from 'ethers' // Add this to your package.json if not already there
|
||||||
|
|
||||||
|
export function DirectKeyAuth() {
|
||||||
|
const [sessionStatus, setSessionStatus] = useState<'checking' | 'authenticated' | 'unauthenticated'>('checking')
|
||||||
|
const [sessionData, setSessionData] = useState<any>(null)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Check if we already have a session
|
||||||
|
const checkSession = async () => {
|
||||||
|
try {
|
||||||
|
setSessionStatus('checking')
|
||||||
|
const response = await fetch('http://localhost:8000/auth/session', {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
setSessionStatus('authenticated')
|
||||||
|
setSessionData(data)
|
||||||
|
console.log('Session check successful:', data)
|
||||||
|
} else {
|
||||||
|
setSessionStatus('unauthenticated')
|
||||||
|
setSessionData(null)
|
||||||
|
console.log('Session check failed:', await response.text())
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking session:', error)
|
||||||
|
setSessionStatus('unauthenticated')
|
||||||
|
setSessionData(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check session on component mount
|
||||||
|
useEffect(() => {
|
||||||
|
checkSession()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Sign in with private key
|
||||||
|
const signInWithKey = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
// Create wallet from private key
|
||||||
|
const privateKey = '0x23ad64eabeba406086636c621893370c32d8678b5c879195ed4616e842b7aa42';
|
||||||
|
const wallet = new Wallet(privateKey);
|
||||||
|
|
||||||
|
// Get the address
|
||||||
|
const address = wallet.address;
|
||||||
|
console.log('Derived address:', address);
|
||||||
|
|
||||||
|
// Create SIWE message
|
||||||
|
const domain = window.location.host;
|
||||||
|
const origin = window.location.origin;
|
||||||
|
const nonce = Math.random().toString(36).slice(2);
|
||||||
|
const issuedAt = new Date().toISOString();
|
||||||
|
|
||||||
|
const message = `${domain} wants you to sign in with your Ethereum account:
|
||||||
|
${address}
|
||||||
|
|
||||||
|
Sign in With Ethereum.
|
||||||
|
|
||||||
|
URI: ${origin}
|
||||||
|
Version: 1
|
||||||
|
Chain ID: 1
|
||||||
|
Nonce: ${nonce}
|
||||||
|
Issued At: ${issuedAt}`;
|
||||||
|
|
||||||
|
console.log('Message to sign:', message);
|
||||||
|
|
||||||
|
// Sign the message
|
||||||
|
const signature = await wallet.signMessage(message);
|
||||||
|
console.log('Generated signature:', signature);
|
||||||
|
|
||||||
|
// Send to backend
|
||||||
|
const response = await fetch('http://localhost:8000/auth/validate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({
|
||||||
|
message,
|
||||||
|
signature
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseData = await response.text();
|
||||||
|
console.log('Response data:', responseData);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
console.log('Authentication successful!');
|
||||||
|
await checkSession();
|
||||||
|
} else {
|
||||||
|
setError(`Authentication failed: ${responseData}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error signing in with key:', error);
|
||||||
|
setError(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 border rounded-md">
|
||||||
|
<h2 className="text-lg font-bold mb-4">Direct Key Authentication</h2>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<p className="text-amber-600 text-sm mb-2">
|
||||||
|
This component uses a local private key to sign messages directly, bypassing the wallet UI.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={signInWithKey}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="mb-2"
|
||||||
|
>
|
||||||
|
{isLoading ? 'Authenticating...' : 'Sign In With Private Key'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-md font-semibold mb-2">Backend Session</h3>
|
||||||
|
<p className="mb-2">
|
||||||
|
Status:
|
||||||
|
<span className={
|
||||||
|
sessionStatus === 'authenticated' ? "text-green-500" :
|
||||||
|
sessionStatus === 'unauthenticated' ? "text-red-500" :
|
||||||
|
"text-yellow-500"
|
||||||
|
}>
|
||||||
|
{' '}{sessionStatus}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{sessionData && (
|
||||||
|
<div className="p-2 bg-gray-800 text-white rounded mb-2">
|
||||||
|
<pre className="font-mono text-sm overflow-auto max-h-32">{JSON.stringify(sessionData, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-2">
|
||||||
|
<Button variant="outline" onClick={checkSession}>Check Session</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mt-2 p-2 bg-red-100 border border-red-300 text-red-800 rounded">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
93
apps/deploy-fe/src/components/GQLTest.tsx
Normal file
93
apps/deploy-fe/src/components/GQLTest.tsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
'use client'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useGQLClient } from '@/context'
|
||||||
|
|
||||||
|
export function GQLTest() {
|
||||||
|
const [testResponse, setTestResponse] = useState<string>('Testing connection...')
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const gqlClient = useGQLClient()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function testGQLConnection() {
|
||||||
|
try {
|
||||||
|
// Try a direct GraphQL query using fetch
|
||||||
|
const response = await fetch('http://localhost:8000/graphql', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include', // Important for sending cookies
|
||||||
|
body: JSON.stringify({
|
||||||
|
query: `
|
||||||
|
{
|
||||||
|
__schema {
|
||||||
|
queryType {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
setTestResponse(JSON.stringify(data, null, 2))
|
||||||
|
|
||||||
|
// Check server logs to see if our request arrived
|
||||||
|
console.log('GraphQL test response:', data)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error testing GraphQL connection:', err)
|
||||||
|
setError(err instanceof Error ? err.message : String(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testGQLConnection()
|
||||||
|
}, [gqlClient])
|
||||||
|
|
||||||
|
// Function to test direct connection
|
||||||
|
const testDirectConnection = async () => {
|
||||||
|
try {
|
||||||
|
setTestResponse('Testing direct connection...')
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
const response = await fetch('http://localhost:8000/auth/session', {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
setTestResponse(JSON.stringify(data, null, 2))
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error testing direct connection:', err)
|
||||||
|
setError(err instanceof Error ? err.message : String(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 border rounded-md shadow-sm">
|
||||||
|
<h2 className="text-lg font-bold mb-2">GraphQL Connection Test</h2>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<button
|
||||||
|
onClick={testDirectConnection}
|
||||||
|
className="px-4 py-2 bg-blue-500 text-white rounded"
|
||||||
|
>
|
||||||
|
Test Direct Connection
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error ? (
|
||||||
|
<div className="text-red-500">
|
||||||
|
<p>Error connecting to GraphQL server:</p>
|
||||||
|
<pre className="bg-gray-900 p-2 rounded overflow-auto max-h-48">{error}</pre>
|
||||||
|
<p className="mt-2">
|
||||||
|
Authentication error is expected without a valid session. The GQL server requires authentication.
|
||||||
|
</p>
|
||||||
|
<p>Check the server logs to see if the request was received.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<pre className="bg-gray-900 p-2 rounded overflow-auto max-h-48">{testResponse}</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
241
apps/deploy-fe/src/components/GitHubBackendAuth.tsx
Normal file
241
apps/deploy-fe/src/components/GitHubBackendAuth.tsx
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
// src/components/GitHubBackendAuth.tsx
|
||||||
|
'use client'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { Button } from '@workspace/ui/components/button'
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@workspace/ui/components/card'
|
||||||
|
import { CheckCircle2, GitBranch, ExternalLink, AlertCircle } from 'lucide-react'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
import { useGQLClient } from '@/context'
|
||||||
|
|
||||||
|
interface GitHubBackendAuthProps {
|
||||||
|
onAuthStatusChange?: (isAuthenticated: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GitHubBackendAuth({ onAuthStatusChange }: GitHubBackendAuthProps) {
|
||||||
|
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [isChecking, setIsChecking] = useState(true)
|
||||||
|
const gqlClient = useGQLClient()
|
||||||
|
|
||||||
|
// GitHub OAuth configuration - replace with your backend OAuth app credentials
|
||||||
|
const GITHUB_CLIENT_ID = process.env.NEXT_PUBLIC_GITHUB_BACKEND_CLIENT_ID || 'your_backend_client_id_here'
|
||||||
|
const REDIRECT_URI = `${window.location.origin}/auth/github/backend-callback`
|
||||||
|
|
||||||
|
// Check current authentication status
|
||||||
|
const checkAuthStatus = async () => {
|
||||||
|
try {
|
||||||
|
setIsChecking(true)
|
||||||
|
const userData = await gqlClient.getUser()
|
||||||
|
const hasGitHubToken = !!userData.user.gitHubToken
|
||||||
|
setIsAuthenticated(hasGitHubToken)
|
||||||
|
onAuthStatusChange?.(hasGitHubToken)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking GitHub auth status:', error)
|
||||||
|
setIsAuthenticated(false)
|
||||||
|
onAuthStatusChange?.(false)
|
||||||
|
} finally {
|
||||||
|
setIsChecking(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check auth status on mount
|
||||||
|
useEffect(() => {
|
||||||
|
checkAuthStatus()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Listen for OAuth callback completion
|
||||||
|
useEffect(() => {
|
||||||
|
const handleAuthComplete = (event: MessageEvent) => {
|
||||||
|
if (event.origin !== window.location.origin) return
|
||||||
|
|
||||||
|
if (event.data.type === 'GITHUB_BACKEND_AUTH_SUCCESS') {
|
||||||
|
toast.success('GitHub backend authentication successful!')
|
||||||
|
checkAuthStatus()
|
||||||
|
} else if (event.data.type === 'GITHUB_BACKEND_AUTH_ERROR') {
|
||||||
|
toast.error(`GitHub authentication failed: ${event.data.message}`)
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('message', handleAuthComplete)
|
||||||
|
return () => window.removeEventListener('message', handleAuthComplete)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const startGitHubAuth = () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
// Generate state parameter for security
|
||||||
|
const state = Math.random().toString(36).substring(2, 15)
|
||||||
|
sessionStorage.setItem('github_oauth_state', state)
|
||||||
|
|
||||||
|
// Build GitHub OAuth URL
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
client_id: GITHUB_CLIENT_ID,
|
||||||
|
redirect_uri: REDIRECT_URI,
|
||||||
|
scope: 'repo public_repo user:email',
|
||||||
|
state: state,
|
||||||
|
response_type: 'code'
|
||||||
|
})
|
||||||
|
|
||||||
|
const authUrl = `https://github.com/login/oauth/authorize?${params.toString()}`
|
||||||
|
|
||||||
|
// Open OAuth in popup window
|
||||||
|
const popup = window.open(
|
||||||
|
authUrl,
|
||||||
|
'github-oauth',
|
||||||
|
'width=600,height=700,scrollbars=yes,resizable=yes'
|
||||||
|
)
|
||||||
|
|
||||||
|
// Monitor popup closure
|
||||||
|
const checkClosed = setInterval(() => {
|
||||||
|
if (popup?.closed) {
|
||||||
|
clearInterval(checkClosed)
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const disconnectGitHub = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
await gqlClient.unauthenticateGithub()
|
||||||
|
setIsAuthenticated(false)
|
||||||
|
onAuthStatusChange?.(false)
|
||||||
|
toast.success('GitHub disconnected successfully')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error disconnecting GitHub:', error)
|
||||||
|
toast.error('Failed to disconnect GitHub')
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isChecking) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>GitHub Backend Authentication</CardTitle>
|
||||||
|
<CardDescription>Checking authentication status...</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-center justify-center p-4">
|
||||||
|
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-gray-900"></div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center">
|
||||||
|
<GitBranch className="mr-2 h-5 w-5" />
|
||||||
|
GitHub Backend Authentication
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Connect your GitHub account to the backend for deployment operations
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className={`w-3 h-3 rounded-full mr-2 ${
|
||||||
|
isAuthenticated ? 'bg-green-500' : 'bg-red-500'
|
||||||
|
}`}></div>
|
||||||
|
<span>
|
||||||
|
Backend GitHub Token: {isAuthenticated ? 'Connected' : 'Not Connected'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isAuthenticated ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="p-3 bg-blue-50 dark:bg-blue-900/20 rounded-md">
|
||||||
|
<div className="flex items-start">
|
||||||
|
<AlertCircle className="h-5 w-5 text-blue-600 mr-2 mt-0.5" />
|
||||||
|
<div className="text-sm text-blue-800 dark:text-blue-200">
|
||||||
|
<p className="font-medium mb-1">Backend GitHub Authentication Required</p>
|
||||||
|
<p>
|
||||||
|
This connects your GitHub account directly to the backend for deployment operations.
|
||||||
|
This is separate from your Clerk GitHub integration.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={startGitHubAuth}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||||
|
Connecting...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ExternalLink className="mr-2 h-4 w-4" />
|
||||||
|
Connect GitHub to Backend
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="text-xs text-gray-500 text-center">
|
||||||
|
This will open GitHub in a popup window for authentication
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="p-3 bg-green-50 dark:bg-green-900/20 rounded-md">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<CheckCircle2 className="h-5 w-5 text-green-600 mr-2" />
|
||||||
|
<div className="text-sm text-green-800 dark:text-green-200">
|
||||||
|
<p className="font-medium">GitHub Backend Connected Successfully</p>
|
||||||
|
<p>Your backend can now access GitHub for deployments</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<Button
|
||||||
|
onClick={checkAuthStatus}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
Refresh Status
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={disconnectGitHub}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="flex-1"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? 'Disconnecting...' : 'Disconnect'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!GITHUB_CLIENT_ID.startsWith('your_') ? null : (
|
||||||
|
<div className="p-3 bg-amber-50 dark:bg-amber-900/20 rounded-md">
|
||||||
|
<div className="flex items-start">
|
||||||
|
<AlertCircle className="h-5 w-5 text-amber-600 mr-2 mt-0.5" />
|
||||||
|
<div className="text-sm text-amber-800 dark:text-amber-200">
|
||||||
|
<p className="font-medium mb-1">Configuration Required</p>
|
||||||
|
<p>
|
||||||
|
Please set <code>NEXT_PUBLIC_GITHUB_BACKEND_CLIENT_ID</code> in your environment variables
|
||||||
|
with your backend GitHub OAuth app client ID.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -10,7 +10,7 @@ import {
|
|||||||
SheetTitle,
|
SheetTitle,
|
||||||
SheetTrigger
|
SheetTrigger
|
||||||
} from '@workspace/ui/components/sheet'
|
} from '@workspace/ui/components/sheet'
|
||||||
import { CreditCard, Menu, Shapes, WalletIcon } from 'lucide-react'
|
import { CreditCard, Menu, Shapes } from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
import { DarkModeToggle } from '../dark-mode-toggle'
|
import { DarkModeToggle } from '../dark-mode-toggle'
|
||||||
@ -202,10 +202,9 @@ export default function TopNavigation({
|
|||||||
config = {
|
config = {
|
||||||
leftItems: [
|
leftItems: [
|
||||||
{ icon: Shapes, label: 'Projects', href: '/projects' },
|
{ icon: Shapes, label: 'Projects', href: '/projects' },
|
||||||
{ icon: WalletIcon, label: 'Wallet', href: '/wallet' },
|
|
||||||
{ icon: CreditCard, label: 'Purchase', href: '/purchase' }
|
|
||||||
],
|
],
|
||||||
rightItems: [
|
rightItems: [
|
||||||
|
{ icon: CreditCard, label: 'Purchase', href: '/purchase' },
|
||||||
{ label: 'Support', href: '/support' },
|
{ label: 'Support', href: '/support' },
|
||||||
{ label: 'Documentation', href: '/documentation' }
|
{ label: 'Documentation', href: '/documentation' }
|
||||||
]
|
]
|
||||||
|
|||||||
@ -67,7 +67,6 @@
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useWallet } from '@/context/WalletContext'
|
import { useWallet } from '@/context/WalletContext'
|
||||||
import { Button } from '@workspace/ui/components/button'
|
import { Button } from '@workspace/ui/components/button'
|
||||||
import {
|
import {
|
||||||
@ -78,36 +77,16 @@ import {
|
|||||||
} from '@workspace/ui/components/dropdown-menu'
|
} from '@workspace/ui/components/dropdown-menu'
|
||||||
import { cn } from '@workspace/ui/lib/utils'
|
import { cn } from '@workspace/ui/lib/utils'
|
||||||
import { ChevronDown, LogOut } from 'lucide-react'
|
import { ChevronDown, LogOut } from 'lucide-react'
|
||||||
import useCheckBalance from '@/components/iframe/check-balance-iframe/useCheckBalance'
|
|
||||||
import { useEffect } from 'react'
|
|
||||||
|
|
||||||
const IFRAME_ID = 'checkBalanceIframe'
|
export function WalletSessionBadge() {
|
||||||
|
|
||||||
export function WalletSessionBadge({ className }: { className?: string }) {
|
|
||||||
const { wallet, isConnected, connect, disconnect } = useWallet()
|
const { wallet, isConnected, connect, disconnect } = useWallet()
|
||||||
const { isBalanceSufficient, checkBalance } = useCheckBalance("1", IFRAME_ID)
|
|
||||||
|
|
||||||
// Check balance when wallet connects
|
// Format address for display
|
||||||
useEffect(() => {
|
|
||||||
if (isConnected) {
|
|
||||||
checkBalance()
|
|
||||||
}
|
|
||||||
}, [isConnected, checkBalance])
|
|
||||||
|
|
||||||
// Format address for display (first 6 chars + ... + last 4 chars)
|
|
||||||
const formatAddress = (address?: string) => {
|
const formatAddress = (address?: string) => {
|
||||||
if (!address) return 'Connect Wallet'
|
if (!address) return 'Connect Wallet'
|
||||||
return `${address.substring(0, 6)}...${address.substring(address.length - 4)}`
|
return `${address.substring(0, 6)}...${address.substring(address.length - 4)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the status indicator color based on connection and balance
|
|
||||||
const getStatusColor = () => {
|
|
||||||
if (!isConnected) return 'bg-red-500'
|
|
||||||
if (isBalanceSufficient === false) return 'bg-yellow-500'
|
|
||||||
if (isBalanceSufficient === true) return 'bg-green-500'
|
|
||||||
return 'bg-blue-500' // Checking balance
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
@ -116,41 +95,37 @@ export function WalletSessionBadge({ className }: { className?: string }) {
|
|||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center space-x-2 rounded-md border px-3 py-1.5 text-sm font-medium',
|
'flex items-center space-x-2 rounded-md border px-3 py-1.5 text-sm font-medium',
|
||||||
'hover:bg-accent hover:text-accent-foreground',
|
'hover:bg-accent hover:text-accent-foreground',
|
||||||
'dark:bg-accent/5 dark:hover:bg-accent/10',
|
'dark:bg-accent/5 dark:hover:bg-accent/10'
|
||||||
className
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="relative flex h-2 w-2">
|
<span className="relative flex h-2 w-2 mr-2">
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'absolute inline-flex h-full w-full animate-ping rounded-full opacity-75',
|
'absolute inline-flex h-full w-full animate-ping rounded-full opacity-75',
|
||||||
getStatusColor()
|
isConnected ? 'bg-green-400' : 'bg-red-400'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative inline-flex h-2 w-2 rounded-full',
|
'relative inline-flex h-2 w-2 rounded-full',
|
||||||
getStatusColor()
|
isConnected ? 'bg-green-500' : 'bg-red-500'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>{isConnected && wallet?.address
|
||||||
{isConnected && wallet?.address
|
? formatAddress(wallet.address)
|
||||||
? formatAddress(wallet.address)
|
: 'Connect Wallet'}</span>
|
||||||
: 'Connect Wallet'}
|
<ChevronDown className="h-4 w-4 ml-2" />
|
||||||
</span>
|
|
||||||
<ChevronDown className="h-4 w-4" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
{isConnected ? (
|
{isConnected ? (
|
||||||
<>
|
<>
|
||||||
<div className="px-2 py-1.5">
|
<div className="px-2 py-1.5">
|
||||||
<p className="text-sm font-medium">Connected to:</p>
|
<p className="text-sm font-medium">Connected Wallet</p>
|
||||||
<p className="text-xs text-muted-foreground truncate">{wallet?.address}</p>
|
<p className="text-xs text-muted-foreground font-mono truncate max-w-[200px]">
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
{wallet?.address}
|
||||||
Balance: {isBalanceSufficient === undefined ? 'Checking...' :
|
|
||||||
isBalanceSufficient ? 'Sufficient' : 'Insufficient'}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
|||||||
@ -29,8 +29,8 @@
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
'use client'
|
'use client'
|
||||||
|
import { useState } from 'react'
|
||||||
import { useWallet } from '@/context/WalletContext' // or WalletContextProvider
|
import { useWallet } from '@/context/WalletContextProvider'
|
||||||
import { Button } from '@workspace/ui/components/button'
|
import { Button } from '@workspace/ui/components/button'
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@ -41,10 +41,17 @@ import {
|
|||||||
import { cn } from '@workspace/ui/lib/utils'
|
import { cn } from '@workspace/ui/lib/utils'
|
||||||
import { ChevronDown, LogOut } from 'lucide-react'
|
import { ChevronDown, LogOut } from 'lucide-react'
|
||||||
|
|
||||||
export function WalletSessionBadge({ className }: { className?: string }) {
|
export function WalletSessionBadge() {
|
||||||
const { wallet, isConnected, connect, disconnect } = useWallet()
|
const { wallet, isConnected, connect, disconnect } = useWallet()
|
||||||
|
const [showAuthModal, setShowAuthModal] = useState(false)
|
||||||
|
|
||||||
// Format address for display (first 6 chars + ... + last 4 chars)
|
const handleConnect = () => {
|
||||||
|
// Instead of directly showing the modal, call the connect function
|
||||||
|
// from WalletContext which should now use the iframe messaging
|
||||||
|
connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format address for display
|
||||||
const formatAddress = (address?: string) => {
|
const formatAddress = (address?: string) => {
|
||||||
if (!address) return 'Connect Wallet'
|
if (!address) return 'Connect Wallet'
|
||||||
return `${address.substring(0, 6)}...${address.substring(address.length - 4)}`
|
return `${address.substring(0, 6)}...${address.substring(address.length - 4)}`
|
||||||
@ -58,8 +65,7 @@ export function WalletSessionBadge({ className }: { className?: string }) {
|
|||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center space-x-2 rounded-md border px-3 py-1.5 text-sm font-medium',
|
'flex items-center space-x-2 rounded-md border px-3 py-1.5 text-sm font-medium',
|
||||||
'hover:bg-accent hover:text-accent-foreground',
|
'hover:bg-accent hover:text-accent-foreground',
|
||||||
'dark:bg-accent/5 dark:hover:bg-accent/10',
|
'dark:bg-accent/5 dark:hover:bg-accent/10'
|
||||||
className
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="relative flex h-2 w-2">
|
<span className="relative flex h-2 w-2">
|
||||||
@ -69,6 +75,7 @@ export function WalletSessionBadge({ className }: { className?: string }) {
|
|||||||
isConnected ? 'bg-green-400' : 'bg-red-400'
|
isConnected ? 'bg-green-400' : 'bg-red-400'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative inline-flex h-2 w-2 rounded-full',
|
'relative inline-flex h-2 w-2 rounded-full',
|
||||||
@ -94,7 +101,7 @@ export function WalletSessionBadge({ className }: { className?: string }) {
|
|||||||
) : (
|
) : (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="flex cursor-pointer items-center"
|
className="flex cursor-pointer items-center"
|
||||||
onClick={connect}
|
onClick={handleConnect}
|
||||||
>
|
>
|
||||||
<span>Connect Wallet</span>
|
<span>Connect Wallet</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|||||||
@ -1,63 +1,213 @@
|
|||||||
|
// 'use client'
|
||||||
|
// import { useCallback, useEffect, useState } from 'react'
|
||||||
|
// // Commenting out these imports as they cause linter errors due to missing dependencies
|
||||||
|
// // In an actual implementation, these would be properly installed
|
||||||
|
// // import { generateNonce, SiweMessage } from 'siwe'
|
||||||
|
// // import axios from 'axios'
|
||||||
|
|
||||||
|
// // Define proper types to replace 'any'
|
||||||
|
// interface SiweMessageProps {
|
||||||
|
// version: string
|
||||||
|
// domain: string
|
||||||
|
// uri: string
|
||||||
|
// chainId: number
|
||||||
|
// address: string
|
||||||
|
// nonce: string
|
||||||
|
// statement: string
|
||||||
|
// }
|
||||||
|
|
||||||
|
// interface ValidateRequestData {
|
||||||
|
// message: string
|
||||||
|
// signature: string
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Mock implementations to demonstrate functionality without dependencies
|
||||||
|
// // In a real project, use the actual dependencies
|
||||||
|
// const generateNonce = () => Math.random().toString(36).substring(2, 15)
|
||||||
|
// const SiweMessage = class {
|
||||||
|
// constructor(props: SiweMessageProps) {
|
||||||
|
// this.props = props
|
||||||
|
// }
|
||||||
|
// props: SiweMessageProps
|
||||||
|
// prepareMessage() {
|
||||||
|
// return JSON.stringify(this.props)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Access environment variables from .env.local with fallbacks for safety
|
||||||
|
// // In a production environment, these would be properly configured
|
||||||
|
// const WALLET_IFRAME_URL =
|
||||||
|
// process.env.NEXT_PUBLIC_WALLET_IFRAME_URL || 'https://wallet.example.com'
|
||||||
|
|
||||||
|
// // Mock axios implementation
|
||||||
|
// const axiosInstance = {
|
||||||
|
// post: async (url: string, data: ValidateRequestData) => {
|
||||||
|
// console.log('Mock API call to', url, 'with data', data)
|
||||||
|
// return { data: { success: true } }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * AutoSignInIFrameModal component that handles wallet authentication through an iframe.
|
||||||
|
// * This component is responsible for:
|
||||||
|
// * 1. Getting the wallet address
|
||||||
|
// * 2. Creating a Sign-In With Ethereum message
|
||||||
|
// * 3. Requesting signature from the wallet
|
||||||
|
// * 4. Validating the signature with the backend
|
||||||
|
// *
|
||||||
|
// * @returns {JSX.Element} A modal with an iframe for wallet authentication
|
||||||
|
// */
|
||||||
|
// export function AutoSignInIFrameModal() {
|
||||||
|
// const [accountAddress, setAccountAddress] = useState<string>()
|
||||||
|
|
||||||
|
// // Handle sign-in response from the wallet iframe
|
||||||
|
// useEffect(() => {
|
||||||
|
// const handleSignInResponse = async (event: MessageEvent) => {
|
||||||
|
// if (event.origin !== WALLET_IFRAME_URL) return
|
||||||
|
|
||||||
|
// if (event.data.type === 'SIGN_IN_RESPONSE') {
|
||||||
|
// try {
|
||||||
|
// const response = await axiosInstance.post('/auth/validate', {
|
||||||
|
// message: event.data.data.message,
|
||||||
|
// signature: event.data.data.signature
|
||||||
|
// })
|
||||||
|
|
||||||
|
// if (response.data.success === true) {
|
||||||
|
// // In Next.js, we would use router.push instead
|
||||||
|
// window.location.href = '/'
|
||||||
|
// }
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('Error signing in:', error)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// window.addEventListener('message', handleSignInResponse)
|
||||||
|
|
||||||
|
// return () => {
|
||||||
|
// window.removeEventListener('message', handleSignInResponse)
|
||||||
|
// }
|
||||||
|
// }, [])
|
||||||
|
|
||||||
|
// // Initiate auto sign-in when account address is available
|
||||||
|
// useEffect(() => {
|
||||||
|
// const initiateAutoSignIn = async () => {
|
||||||
|
// if (!accountAddress) return
|
||||||
|
|
||||||
|
// const iframe = document.getElementById(
|
||||||
|
// 'walletAuthFrame'
|
||||||
|
// ) as HTMLIFrameElement
|
||||||
|
|
||||||
|
// if (!iframe.contentWindow) {
|
||||||
|
// console.error('Iframe not found or not loaded')
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const message = new SiweMessage({
|
||||||
|
// version: '1',
|
||||||
|
// domain: window.location.host,
|
||||||
|
// uri: window.location.origin,
|
||||||
|
// chainId: 1,
|
||||||
|
// address: accountAddress,
|
||||||
|
// nonce: generateNonce(),
|
||||||
|
// statement: 'Sign in With Ethereum.'
|
||||||
|
// }).prepareMessage()
|
||||||
|
|
||||||
|
// iframe.contentWindow.postMessage(
|
||||||
|
// {
|
||||||
|
// type: 'AUTO_SIGN_IN',
|
||||||
|
// chainId: '1',
|
||||||
|
// message
|
||||||
|
// },
|
||||||
|
// WALLET_IFRAME_URL
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// initiateAutoSignIn()
|
||||||
|
// }, [accountAddress])
|
||||||
|
|
||||||
|
// // Listen for wallet accounts data
|
||||||
|
// useEffect(() => {
|
||||||
|
// const handleAccountsDataResponse = async (event: MessageEvent) => {
|
||||||
|
// if (event.origin !== WALLET_IFRAME_URL) return
|
||||||
|
|
||||||
|
// if (
|
||||||
|
// event.data.type === 'WALLET_ACCOUNTS_DATA' &&
|
||||||
|
// event.data.data?.length > 0
|
||||||
|
// ) {
|
||||||
|
// setAccountAddress(event.data.data[0].address)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// window.addEventListener('message', handleAccountsDataResponse)
|
||||||
|
|
||||||
|
// return () => {
|
||||||
|
// window.removeEventListener('message', handleAccountsDataResponse)
|
||||||
|
// }
|
||||||
|
// }, [])
|
||||||
|
|
||||||
|
// // Request wallet address when iframe is loaded
|
||||||
|
// const getAddressFromWallet = useCallback(() => {
|
||||||
|
// const iframe = document.getElementById(
|
||||||
|
// 'walletAuthFrame'
|
||||||
|
// ) as HTMLIFrameElement
|
||||||
|
|
||||||
|
// if (!iframe.contentWindow) {
|
||||||
|
// console.error('Iframe not found or not loaded')
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// iframe.contentWindow.postMessage(
|
||||||
|
// {
|
||||||
|
// type: 'REQUEST_CREATE_OR_GET_ACCOUNTS',
|
||||||
|
// chainId: '1'
|
||||||
|
// },
|
||||||
|
// WALLET_IFRAME_URL
|
||||||
|
// )
|
||||||
|
// }, [])
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||||
|
// <div className="relative w-[90%] max-w-6xl h-[600px] max-h-[80vh] overflow-auto rounded-lg bg-white shadow-lg">
|
||||||
|
// <iframe
|
||||||
|
// onLoad={getAddressFromWallet}
|
||||||
|
// id="walletAuthFrame"
|
||||||
|
// src={`${WALLET_IFRAME_URL}/auto-sign-in`}
|
||||||
|
// className="w-full h-full"
|
||||||
|
// sandbox="allow-scripts allow-same-origin"
|
||||||
|
// title="Wallet Authentication"
|
||||||
|
// />
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// src/components/iframe/auto-sign-in/AutoSignInIFrameModal.tsx
|
||||||
|
'use client'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
// Commenting out these imports as they cause linter errors due to missing dependencies
|
import { generateNonce, SiweMessage } from 'siwe'
|
||||||
// In an actual implementation, these would be properly installed
|
import axios from 'axios'
|
||||||
// import { generateNonce, SiweMessage } from 'siwe'
|
|
||||||
// import axios from 'axios'
|
|
||||||
|
|
||||||
// Define proper types to replace 'any'
|
const axiosInstance = axios.create({
|
||||||
interface SiweMessageProps {
|
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000',
|
||||||
version: string
|
headers: {
|
||||||
domain: string
|
'Content-Type': 'application/json',
|
||||||
uri: string
|
'Access-Control-Allow-Origin': '*'
|
||||||
chainId: number
|
},
|
||||||
address: string
|
withCredentials: true
|
||||||
nonce: string
|
})
|
||||||
statement: string
|
|
||||||
|
const WALLET_IFRAME_URL = process.env.NEXT_PUBLIC_WALLET_IFRAME_URL || 'http://localhost:4000'
|
||||||
|
|
||||||
|
interface AutoSignInProps {
|
||||||
|
onAuthComplete?: (success: boolean) => void
|
||||||
|
onClose?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ValidateRequestData {
|
export function AutoSignInIFrameModal({ onAuthComplete, onClose }: AutoSignInProps = {}) {
|
||||||
message: string
|
|
||||||
signature: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock implementations to demonstrate functionality without dependencies
|
|
||||||
// In a real project, use the actual dependencies
|
|
||||||
const generateNonce = () => Math.random().toString(36).substring(2, 15)
|
|
||||||
const SiweMessage = class {
|
|
||||||
constructor(props: SiweMessageProps) {
|
|
||||||
this.props = props
|
|
||||||
}
|
|
||||||
props: SiweMessageProps
|
|
||||||
prepareMessage() {
|
|
||||||
return JSON.stringify(this.props)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Access environment variables from .env.local with fallbacks for safety
|
|
||||||
// In a production environment, these would be properly configured
|
|
||||||
const WALLET_IFRAME_URL =
|
|
||||||
process.env.NEXT_PUBLIC_WALLET_IFRAME_URL || 'https://wallet.example.com'
|
|
||||||
|
|
||||||
// Mock axios implementation
|
|
||||||
const axiosInstance = {
|
|
||||||
post: async (url: string, data: ValidateRequestData) => {
|
|
||||||
console.log('Mock API call to', url, 'with data', data)
|
|
||||||
return { data: { success: true } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AutoSignInIFrameModal component that handles wallet authentication through an iframe.
|
|
||||||
* This component is responsible for:
|
|
||||||
* 1. Getting the wallet address
|
|
||||||
* 2. Creating a Sign-In With Ethereum message
|
|
||||||
* 3. Requesting signature from the wallet
|
|
||||||
* 4. Validating the signature with the backend
|
|
||||||
*
|
|
||||||
* @returns {JSX.Element} A modal with an iframe for wallet authentication
|
|
||||||
*/
|
|
||||||
export function AutoSignInIFrameModal() {
|
|
||||||
const [accountAddress, setAccountAddress] = useState<string>()
|
const [accountAddress, setAccountAddress] = useState<string>()
|
||||||
|
const [isVisible, setIsVisible] = useState(true)
|
||||||
|
const [authStatus, setAuthStatus] = useState<'idle' | 'connecting' | 'signing' | 'success' | 'error'>('idle')
|
||||||
|
|
||||||
// Handle sign-in response from the wallet iframe
|
// Handle sign-in response from the wallet iframe
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -66,26 +216,70 @@ export function AutoSignInIFrameModal() {
|
|||||||
|
|
||||||
if (event.data.type === 'SIGN_IN_RESPONSE') {
|
if (event.data.type === 'SIGN_IN_RESPONSE') {
|
||||||
try {
|
try {
|
||||||
|
setAuthStatus('signing')
|
||||||
|
console.log('🔐 Validating SIWE signature...')
|
||||||
|
|
||||||
const response = await axiosInstance.post('/auth/validate', {
|
const response = await axiosInstance.post('/auth/validate', {
|
||||||
message: event.data.data.message,
|
message: event.data.data.message,
|
||||||
signature: event.data.data.signature
|
signature: event.data.data.signature
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response.data.success === true) {
|
if (response.data.success === true) {
|
||||||
// In Next.js, we would use router.push instead
|
console.log('✅ SIWE authentication successful!')
|
||||||
window.location.href = '/'
|
setAuthStatus('success')
|
||||||
|
|
||||||
|
// Notify parent component instead of redirecting
|
||||||
|
onAuthComplete?.(true)
|
||||||
|
|
||||||
|
// Close modal after a brief delay
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsVisible(false)
|
||||||
|
onClose?.()
|
||||||
|
}, 1000)
|
||||||
|
} else {
|
||||||
|
console.error('❌ SIWE authentication failed')
|
||||||
|
setAuthStatus('error')
|
||||||
|
onAuthComplete?.(false)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error signing in:', error)
|
console.error('❌ Error during SIWE validation:', error)
|
||||||
|
setAuthStatus('error')
|
||||||
|
onAuthComplete?.(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('message', handleSignInResponse)
|
window.addEventListener('message', handleSignInResponse)
|
||||||
|
return () => window.removeEventListener('message', handleSignInResponse)
|
||||||
|
}, [onAuthComplete, onClose])
|
||||||
|
|
||||||
return () => {
|
// Listen for wallet accounts data
|
||||||
window.removeEventListener('message', handleSignInResponse)
|
useEffect(() => {
|
||||||
|
const handleAccountsDataResponse = async (event: MessageEvent) => {
|
||||||
|
if (event.origin !== WALLET_IFRAME_URL) return
|
||||||
|
|
||||||
|
if (event.data.type === 'WALLET_ACCOUNTS_DATA' && event.data.data?.length > 0) {
|
||||||
|
let address;
|
||||||
|
|
||||||
|
// Handle multiple data formats
|
||||||
|
if (Array.isArray(event.data.data)) {
|
||||||
|
if (typeof event.data.data[0] === 'string') {
|
||||||
|
address = event.data.data[0];
|
||||||
|
} else if (event.data.data[0] && typeof event.data.data[0].address === 'string') {
|
||||||
|
address = event.data.data[0].address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (address) {
|
||||||
|
console.log('📱 Got wallet address for SIWE:', address)
|
||||||
|
setAccountAddress(address)
|
||||||
|
setAuthStatus('connecting')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.addEventListener('message', handleAccountsDataResponse)
|
||||||
|
return () => window.removeEventListener('message', handleAccountsDataResponse)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Initiate auto sign-in when account address is available
|
// Initiate auto sign-in when account address is available
|
||||||
@ -93,12 +287,13 @@ export function AutoSignInIFrameModal() {
|
|||||||
const initiateAutoSignIn = async () => {
|
const initiateAutoSignIn = async () => {
|
||||||
if (!accountAddress) return
|
if (!accountAddress) return
|
||||||
|
|
||||||
const iframe = document.getElementById(
|
console.log('🔐 Starting SIWE authentication for:', accountAddress)
|
||||||
'walletAuthFrame'
|
|
||||||
) as HTMLIFrameElement
|
|
||||||
|
|
||||||
if (!iframe.contentWindow) {
|
const iframe = document.getElementById('walletAuthFrame') as HTMLIFrameElement
|
||||||
console.error('Iframe not found or not loaded')
|
|
||||||
|
if (!iframe?.contentWindow) {
|
||||||
|
console.error('❌ walletAuthFrame iframe not found')
|
||||||
|
setAuthStatus('error')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,6 +307,8 @@ export function AutoSignInIFrameModal() {
|
|||||||
statement: 'Sign in With Ethereum.'
|
statement: 'Sign in With Ethereum.'
|
||||||
}).prepareMessage()
|
}).prepareMessage()
|
||||||
|
|
||||||
|
console.log('📝 SIWE message created, requesting signature...')
|
||||||
|
|
||||||
iframe.contentWindow.postMessage(
|
iframe.contentWindow.postMessage(
|
||||||
{
|
{
|
||||||
type: 'AUTO_SIGN_IN',
|
type: 'AUTO_SIGN_IN',
|
||||||
@ -122,40 +319,23 @@ export function AutoSignInIFrameModal() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
initiateAutoSignIn()
|
if (accountAddress && authStatus === 'connecting') {
|
||||||
}, [accountAddress])
|
setTimeout(initiateAutoSignIn, 500)
|
||||||
|
|
||||||
// Listen for wallet accounts data
|
|
||||||
useEffect(() => {
|
|
||||||
const handleAccountsDataResponse = async (event: MessageEvent) => {
|
|
||||||
if (event.origin !== WALLET_IFRAME_URL) return
|
|
||||||
|
|
||||||
if (
|
|
||||||
event.data.type === 'WALLET_ACCOUNTS_DATA' &&
|
|
||||||
event.data.data?.length > 0
|
|
||||||
) {
|
|
||||||
setAccountAddress(event.data.data[0].address)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}, [accountAddress, authStatus])
|
||||||
window.addEventListener('message', handleAccountsDataResponse)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('message', handleAccountsDataResponse)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Request wallet address when iframe is loaded
|
// Request wallet address when iframe is loaded
|
||||||
const getAddressFromWallet = useCallback(() => {
|
const getAddressFromWallet = useCallback(() => {
|
||||||
const iframe = document.getElementById(
|
const iframe = document.getElementById('walletAuthFrame') as HTMLIFrameElement
|
||||||
'walletAuthFrame'
|
|
||||||
) as HTMLIFrameElement
|
|
||||||
|
|
||||||
if (!iframe.contentWindow) {
|
if (!iframe?.contentWindow) {
|
||||||
console.error('Iframe not found or not loaded')
|
console.error('❌ Iframe not found or not loaded')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('📤 Requesting wallet address for SIWE...')
|
||||||
|
setAuthStatus('idle')
|
||||||
|
|
||||||
iframe.contentWindow.postMessage(
|
iframe.contentWindow.postMessage(
|
||||||
{
|
{
|
||||||
type: 'REQUEST_CREATE_OR_GET_ACCOUNTS',
|
type: 'REQUEST_CREATE_OR_GET_ACCOUNTS',
|
||||||
@ -165,9 +345,61 @@ export function AutoSignInIFrameModal() {
|
|||||||
)
|
)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Don't render if not visible
|
||||||
|
if (!isVisible) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||||
<div className="relative w-[90%] max-w-6xl h-[600px] max-h-[80vh] overflow-auto rounded-lg bg-white shadow-lg">
|
<div className="relative w-[90%] max-w-6xl h-[600px] max-h-[80vh] overflow-auto rounded-lg bg-white shadow-lg">
|
||||||
|
{/* Status indicator */}
|
||||||
|
<div className="absolute top-4 left-4 z-10 bg-white/90 px-3 py-2 rounded-md shadow-sm">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{authStatus === 'idle' && (
|
||||||
|
<>
|
||||||
|
<div className="w-2 h-2 bg-gray-400 rounded-full"></div>
|
||||||
|
<span className="text-sm text-gray-600">Initializing...</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{authStatus === 'connecting' && (
|
||||||
|
<>
|
||||||
|
<div className="w-2 h-2 bg-blue-400 rounded-full animate-pulse"></div>
|
||||||
|
<span className="text-sm text-blue-600">Connecting wallet...</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{authStatus === 'signing' && (
|
||||||
|
<>
|
||||||
|
<div className="w-2 h-2 bg-yellow-400 rounded-full animate-pulse"></div>
|
||||||
|
<span className="text-sm text-yellow-600">Signing message...</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{authStatus === 'success' && (
|
||||||
|
<>
|
||||||
|
<div className="w-2 h-2 bg-green-400 rounded-full"></div>
|
||||||
|
<span className="text-sm text-green-600">Authentication complete!</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{authStatus === 'error' && (
|
||||||
|
<>
|
||||||
|
<div className="w-2 h-2 bg-red-400 rounded-full"></div>
|
||||||
|
<span className="text-sm text-red-600">Authentication failed</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Close button */}
|
||||||
|
{onClose && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsVisible(false)
|
||||||
|
onClose()
|
||||||
|
}}
|
||||||
|
className="absolute top-4 right-4 z-10 bg-white/90 hover:bg-white px-2 py-1 rounded-md shadow-sm text-gray-600 hover:text-gray-800"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
<iframe
|
<iframe
|
||||||
onLoad={getAddressFromWallet}
|
onLoad={getAddressFromWallet}
|
||||||
id="walletAuthFrame"
|
id="walletAuthFrame"
|
||||||
@ -179,4 +411,4 @@ export function AutoSignInIFrameModal() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -1,20 +1,15 @@
|
|||||||
|
// src/components/iframe/check-balance-iframe/CheckBalanceIframe.tsx
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { VisuallyHidden } from '@radix-ui/react-visually-hidden'
|
|
||||||
import { Dialog } from '@workspace/ui/components/dialog'
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import useCheckBalance from './useCheckBalance'
|
import useCheckBalance from './useCheckBalance'
|
||||||
|
|
||||||
const CHECK_BALANCE_INTERVAL = 5000
|
const CHECK_BALANCE_INTERVAL = 5000
|
||||||
const IFRAME_ID = 'checkBalanceIframe'
|
const BALANCE_IFRAME_ID = 'balance-check-iframe' // Different ID to avoid conflicts
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CheckBalanceIframe component that checks the balance using an iframe.
|
* CheckBalanceIframe component for balance checking only.
|
||||||
* @param {Object} props - The component props.
|
* This is separate from wallet connection functionality.
|
||||||
* @param {function} props.onBalanceChange - Callback function to be called when the balance changes.
|
|
||||||
* @param {boolean} props.isPollingEnabled - Determines whether to poll the balance periodically.
|
|
||||||
* @param {string} props.amount - The amount to check against the balance.
|
|
||||||
* @returns {JSX.Element} - The CheckBalanceIframe component.
|
|
||||||
*/
|
*/
|
||||||
const CheckBalanceIframe = ({
|
const CheckBalanceIframe = ({
|
||||||
onBalanceChange,
|
onBalanceChange,
|
||||||
@ -27,24 +22,18 @@ const CheckBalanceIframe = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { isBalanceSufficient, checkBalance } = useCheckBalance(
|
const { isBalanceSufficient, checkBalance } = useCheckBalance(
|
||||||
amount,
|
amount,
|
||||||
IFRAME_ID
|
BALANCE_IFRAME_ID
|
||||||
)
|
)
|
||||||
|
|
||||||
const [isLoaded, setIsLoaded] = useState(false)
|
const [isLoaded, setIsLoaded] = useState(false)
|
||||||
|
|
||||||
/**
|
// Check balance when loaded or amount changes
|
||||||
* useEffect hook that calls checkBalance when the component is loaded or the amount changes.
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoaded) {
|
if (!isLoaded) return
|
||||||
return
|
|
||||||
}
|
|
||||||
checkBalance()
|
checkBalance()
|
||||||
}, [checkBalance, isLoaded])
|
}, [checkBalance, isLoaded])
|
||||||
/**
|
|
||||||
* useEffect hook that sets up an interval to poll the balance if polling is enabled.
|
// Set up polling if enabled
|
||||||
* Clears the interval when the component unmounts, balance is sufficient, or polling is disabled.
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isPollingEnabled || !isLoaded || isBalanceSufficient) {
|
if (!isPollingEnabled || !isLoaded || isBalanceSufficient) {
|
||||||
return
|
return
|
||||||
@ -54,34 +43,34 @@ const CheckBalanceIframe = ({
|
|||||||
checkBalance()
|
checkBalance()
|
||||||
}, CHECK_BALANCE_INTERVAL)
|
}, CHECK_BALANCE_INTERVAL)
|
||||||
|
|
||||||
return () => {
|
return () => clearInterval(interval)
|
||||||
clearInterval(interval)
|
|
||||||
}
|
|
||||||
}, [isBalanceSufficient, isPollingEnabled, checkBalance, isLoaded])
|
}, [isBalanceSufficient, isPollingEnabled, checkBalance, isLoaded])
|
||||||
|
|
||||||
/**
|
// Notify parent of balance changes
|
||||||
* useEffect hook that calls the onBalanceChange callback when the isBalanceSufficient state changes.
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onBalanceChange(isBalanceSufficient)
|
onBalanceChange(isBalanceSufficient)
|
||||||
}, [isBalanceSufficient, onBalanceChange])
|
}, [isBalanceSufficient, onBalanceChange])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={false}>
|
<div style={{ display: 'none' }}>
|
||||||
<VisuallyHidden>
|
{/* This iframe is only for balance checking, not wallet connection */}
|
||||||
<iframe
|
<iframe
|
||||||
title="Check Balance"
|
title="Balance Checker"
|
||||||
onLoad={() => setIsLoaded(true)}
|
onLoad={() => setIsLoaded(true)}
|
||||||
id={IFRAME_ID}
|
id={BALANCE_IFRAME_ID}
|
||||||
src={process.env.NEXT_PUBLIC_WALLET_IFRAME_URL}
|
src={process.env.NEXT_PUBLIC_WALLET_IFRAME_URL}
|
||||||
width="100%"
|
width="1"
|
||||||
height="100%"
|
height="1"
|
||||||
sandbox="allow-scripts allow-same-origin"
|
sandbox="allow-scripts allow-same-origin"
|
||||||
className="border rounded-md shadow-sm"
|
style={{
|
||||||
/>
|
position: 'absolute',
|
||||||
</VisuallyHidden>
|
left: '-9999px',
|
||||||
</Dialog>
|
opacity: 0,
|
||||||
|
pointerEvents: 'none'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CheckBalanceIframe
|
export default CheckBalanceIframe
|
||||||
@ -1,23 +1,49 @@
|
|||||||
|
// src/components/onboarding/configure-step/configure-step.tsx
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { PlusCircle } from 'lucide-react'
|
import { PlusCircle, Loader2, AlertTriangle, Info } from 'lucide-react'
|
||||||
import { useTheme } from 'next-themes'
|
import { useTheme } from 'next-themes'
|
||||||
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
||||||
|
import { useGQLClient } from '@/context'
|
||||||
|
import { useWallet } from '@/context/WalletContext'
|
||||||
import { Button } from '@workspace/ui/components/button'
|
import { Button } from '@workspace/ui/components/button'
|
||||||
import { Input } from '@workspace/ui/components/input'
|
import { Input } from '@workspace/ui/components/input'
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@workspace/ui/components/select'
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@workspace/ui/components/select'
|
||||||
import { Checkbox } from '@workspace/ui/components/checkbox'
|
import { Checkbox } from '@workspace/ui/components/checkbox'
|
||||||
import { Label } from '@workspace/ui/components/label'
|
import { Label } from '@workspace/ui/components/label'
|
||||||
|
import { Alert, AlertDescription } from '@workspace/ui/components/alert'
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card'
|
||||||
|
import { Badge } from '@workspace/ui/components/badge'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
|
interface Deployer {
|
||||||
|
deployerLrn: string
|
||||||
|
deployerApiUrl: string
|
||||||
|
minimumPayment?: string
|
||||||
|
baseDomain: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Organization {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
slug: string
|
||||||
|
}
|
||||||
|
|
||||||
export function ConfigureStep() {
|
export function ConfigureStep() {
|
||||||
const { nextStep, previousStep, setFormData, formData } = useOnboarding()
|
const { nextStep, previousStep, setFormData, formData } = useOnboarding()
|
||||||
const { resolvedTheme } = useTheme()
|
const { resolvedTheme } = useTheme()
|
||||||
const [mounted, setMounted] = useState(false)
|
const [mounted, setMounted] = useState(false)
|
||||||
|
|
||||||
|
// Backend data
|
||||||
|
const [deployers, setDeployers] = useState<Deployer[]>([])
|
||||||
|
const [organizations, setOrganizations] = useState<Organization[]>([])
|
||||||
|
const [isLoadingDeployers, setIsLoadingDeployers] = useState(true)
|
||||||
|
const [isLoadingOrgs, setIsLoadingOrgs] = useState(true)
|
||||||
|
|
||||||
// Form state
|
// Form state
|
||||||
const [deployOption, setDeployOption] = useState<'auction' | 'lrn'>(
|
const [deployOption, setDeployOption] = useState<'auction' | 'lrn'>(
|
||||||
formData.deploymentType as ('auction' | 'lrn') || 'auction'
|
formData.deploymentType as ('auction' | 'lrn') || 'lrn' // Default to LRN for simplicity
|
||||||
)
|
)
|
||||||
const [numberOfDeployers, setNumberOfDeployers] = useState<string>(
|
const [numberOfDeployers, setNumberOfDeployers] = useState<string>(
|
||||||
formData.deployerCount || "1"
|
formData.deployerCount || "1"
|
||||||
@ -28,38 +54,115 @@ export function ConfigureStep() {
|
|||||||
const [selectedLrn, setSelectedLrn] = useState<string>(
|
const [selectedLrn, setSelectedLrn] = useState<string>(
|
||||||
formData.selectedLrn || ""
|
formData.selectedLrn || ""
|
||||||
)
|
)
|
||||||
const [selectedAccount, setSelectedAccount] = useState<string>("")
|
const [selectedOrg, setSelectedOrg] = useState<string>(
|
||||||
const [environments, setEnvironments] = useState<{
|
formData.selectedOrg || ""
|
||||||
production: boolean,
|
)
|
||||||
preview: boolean,
|
const [envVars, setEnvVars] = useState<{ key: string; value: string; environments: string[] }[]>([
|
||||||
development: boolean
|
{ key: '', value: '', environments: ['Production'] }
|
||||||
}>(formData.environments || {
|
|
||||||
production: false,
|
|
||||||
preview: false,
|
|
||||||
development: false
|
|
||||||
})
|
|
||||||
const [envVars, setEnvVars] = useState<{ key: string; value: string }[]>([
|
|
||||||
{ key: '', value: '' }
|
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// Contexts
|
||||||
|
const gqlClient = useGQLClient()
|
||||||
|
const { wallet } = useWallet()
|
||||||
|
|
||||||
// Handle hydration mismatch by waiting for mount
|
// Handle hydration mismatch by waiting for mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true)
|
setMounted(true)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Fetch deployers and organizations on mount
|
||||||
|
useEffect(() => {
|
||||||
|
if (mounted) {
|
||||||
|
fetchDeployers()
|
||||||
|
fetchOrganizations()
|
||||||
|
}
|
||||||
|
}, [mounted])
|
||||||
|
|
||||||
// Initialize environment variables from formData if available
|
// Initialize environment variables from formData if available
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (formData.environmentVariables) {
|
if (formData.environmentVariables && Array.isArray(formData.environmentVariables)) {
|
||||||
const vars: { key: string; value: string }[] = Object.entries(formData.environmentVariables).map(
|
setEnvVars(formData.environmentVariables.length > 0 ? formData.environmentVariables : [
|
||||||
([key, value]) => ({ key, value })
|
{ key: '', value: '', environments: ['Production'] }
|
||||||
)
|
])
|
||||||
setEnvVars(vars.length > 0 ? vars : [{ key: '', value: '' }])
|
|
||||||
}
|
}
|
||||||
}, [formData.environmentVariables])
|
}, [formData.environmentVariables])
|
||||||
|
|
||||||
|
// Fetch deployers from backend
|
||||||
|
const fetchDeployers = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoadingDeployers(true)
|
||||||
|
const deployersData = await gqlClient.getDeployers()
|
||||||
|
console.log('Available deployers:', deployersData)
|
||||||
|
setDeployers(deployersData.deployers || [])
|
||||||
|
|
||||||
|
// Auto-select first deployer if available and none selected
|
||||||
|
if (deployersData.deployers && deployersData.deployers.length > 0 && !selectedLrn) {
|
||||||
|
setSelectedLrn(deployersData.deployers[0].deployerLrn)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching deployers:', error)
|
||||||
|
toast.error('Failed to load deployers')
|
||||||
|
} finally {
|
||||||
|
setIsLoadingDeployers(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch organizations from backend
|
||||||
|
const fetchOrganizations = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoadingOrgs(true)
|
||||||
|
const orgsData = await gqlClient.getOrganizations()
|
||||||
|
console.log('Available organizations:', orgsData)
|
||||||
|
setOrganizations(orgsData.organizations || [])
|
||||||
|
|
||||||
|
// Auto-select first organization if available and none selected
|
||||||
|
if (orgsData.organizations && orgsData.organizations.length > 0 && !selectedOrg) {
|
||||||
|
setSelectedOrg(orgsData.organizations[0].slug)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching organizations:', error)
|
||||||
|
toast.error('Failed to load organizations')
|
||||||
|
} finally {
|
||||||
|
setIsLoadingOrgs(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add an empty environment variable row
|
// Add an empty environment variable row
|
||||||
const addEnvVar = () => {
|
const addEnvVar = () => {
|
||||||
setEnvVars([...envVars, { key: '', value: '' }])
|
setEnvVars([...envVars, { key: '', value: '', environments: ['Production'] }])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove environment variable row
|
||||||
|
const removeEnvVar = (index: number) => {
|
||||||
|
if (envVars.length > 1) {
|
||||||
|
setEnvVars(envVars.filter((_, i) => i !== index))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update environment variable
|
||||||
|
const updateEnvVar = (index: number, field: 'key' | 'value', value: string) => {
|
||||||
|
const newEnvVars = [...envVars]
|
||||||
|
newEnvVars[index][field] = value
|
||||||
|
setEnvVars(newEnvVars)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle environment for variable
|
||||||
|
const toggleEnvironment = (index: number, environment: string) => {
|
||||||
|
const newEnvVars = [...envVars]
|
||||||
|
const currentEnvs = newEnvVars[index].environments
|
||||||
|
|
||||||
|
if (currentEnvs.includes(environment)) {
|
||||||
|
newEnvVars[index].environments = currentEnvs.filter(env => env !== environment)
|
||||||
|
} else {
|
||||||
|
newEnvVars[index].environments = [...currentEnvs, environment]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure at least one environment is selected
|
||||||
|
if (newEnvVars[index].environments.length === 0) {
|
||||||
|
newEnvVars[index].environments = ['Production']
|
||||||
|
}
|
||||||
|
|
||||||
|
setEnvVars(newEnvVars)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle deployment option
|
// Toggle deployment option
|
||||||
@ -67,29 +170,36 @@ export function ConfigureStep() {
|
|||||||
setDeployOption(option)
|
setDeployOption(option)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle environment checkbox
|
// Get selected deployer details
|
||||||
const toggleEnvironment = (env: 'production' | 'preview' | 'development') => {
|
const selectedDeployer = deployers.find(d => d.deployerLrn === selectedLrn)
|
||||||
setEnvironments({
|
|
||||||
...environments,
|
// Validate form
|
||||||
[env]: !environments[env]
|
const canProceed = () => {
|
||||||
})
|
if (deployOption === 'lrn' && !selectedLrn) return false
|
||||||
|
if (!selectedOrg) return false
|
||||||
|
if (!wallet?.address) return false
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle next step
|
// Handle next step
|
||||||
const handleNext = () => {
|
const handleNext = () => {
|
||||||
|
if (!canProceed()) {
|
||||||
|
toast.error('Please complete all required fields')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out empty environment variables
|
||||||
|
const validEnvVars = envVars.filter(env => env.key.trim() && env.value.trim())
|
||||||
|
|
||||||
// Save configuration to form data
|
// Save configuration to form data
|
||||||
setFormData({
|
setFormData({
|
||||||
deploymentType: deployOption,
|
deploymentType: deployOption,
|
||||||
deployerCount: numberOfDeployers,
|
deployerCount: numberOfDeployers,
|
||||||
maxPrice: maxPrice,
|
maxPrice: maxPrice,
|
||||||
selectedLrn: selectedLrn,
|
selectedLrn: selectedLrn,
|
||||||
environments: environments,
|
selectedOrg: selectedOrg,
|
||||||
environmentVariables: envVars.reduce((acc, { key, value }) => {
|
paymentAddress: wallet?.address,
|
||||||
if (key && value) {
|
environmentVariables: validEnvVars
|
||||||
acc[key] = value
|
|
||||||
}
|
|
||||||
return acc
|
|
||||||
}, {} as Record<string, string>)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
nextStep()
|
nextStep()
|
||||||
@ -103,6 +213,10 @@ export function ConfigureStep() {
|
|||||||
// Determine if dark mode is active
|
// Determine if dark mode is active
|
||||||
const isDarkMode = resolvedTheme === 'dark'
|
const isDarkMode = resolvedTheme === 'dark'
|
||||||
|
|
||||||
|
// Get deployment mode info
|
||||||
|
const isTemplateMode = formData.deploymentMode === 'template'
|
||||||
|
const selectedItem = isTemplateMode ? formData.template?.name : formData.githubRepo
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full flex flex-col p-8 overflow-y-auto">
|
<div className="w-full h-full flex flex-col p-8 overflow-y-auto">
|
||||||
{/* Configure icon and header */}
|
{/* Configure icon and header */}
|
||||||
@ -115,119 +229,250 @@ export function ConfigureStep() {
|
|||||||
</div>
|
</div>
|
||||||
<h2 className={`text-2xl font-medium text-center mb-2 ${isDarkMode ? "text-white" : "text-zinc-900"}`}>Configure</h2>
|
<h2 className={`text-2xl font-medium text-center mb-2 ${isDarkMode ? "text-white" : "text-zinc-900"}`}>Configure</h2>
|
||||||
<p className={`text-center text-zinc-500 max-w-md`}>
|
<p className={`text-center text-zinc-500 max-w-md`}>
|
||||||
Set the deployer LRN for a single deployment or by creating a deployer auction for multiple deployments
|
Define the deployment type
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-w-xl mx-auto w-full">
|
<div className="max-w-xl mx-auto w-full">
|
||||||
{/* Deployment options */}
|
{/* Project Summary */}
|
||||||
<div className="grid grid-cols-2 gap-2 mb-6">
|
<Card className="mb-6">
|
||||||
<Button
|
<CardHeader className="pb-3">
|
||||||
variant={deployOption === 'auction' ? "default" : "outline"}
|
<CardTitle className="text-sm flex items-center gap-2">
|
||||||
className={`py-3 ${deployOption === 'auction'
|
<Info className="h-4 w-4" />
|
||||||
? (isDarkMode ? 'bg-zinc-800 text-white' : 'bg-zinc-800 text-white')
|
Project Summary
|
||||||
: (isDarkMode ? 'bg-transparent border-zinc-700 text-zinc-400' : 'bg-transparent border-zinc-300 text-zinc-600')}`}
|
</CardTitle>
|
||||||
onClick={() => toggleDeployOption('auction')}
|
</CardHeader>
|
||||||
>
|
<CardContent className="pt-0">
|
||||||
Create Auction
|
<div className="space-y-2 text-sm">
|
||||||
</Button>
|
<div className="flex justify-between">
|
||||||
<Button
|
<span className="text-muted-foreground">Type:</span>
|
||||||
variant={deployOption === 'lrn' ? "default" : "outline"}
|
<Badge variant="secondary">{isTemplateMode ? 'Template' : 'Repository'}</Badge>
|
||||||
className={`py-3 ${deployOption === 'lrn'
|
|
||||||
? (isDarkMode ? 'bg-zinc-800 text-white' : 'bg-zinc-800 text-white')
|
|
||||||
: (isDarkMode ? 'bg-transparent border-zinc-700 text-zinc-400' : 'bg-transparent border-zinc-300 text-zinc-600')}`}
|
|
||||||
onClick={() => toggleDeployOption('lrn')}
|
|
||||||
>
|
|
||||||
Deployer LRN
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{deployOption === 'auction' ? (
|
|
||||||
<>
|
|
||||||
{/* Auction settings */}
|
|
||||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="deployers" className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}>
|
|
||||||
Number of Deployers
|
|
||||||
</Label>
|
|
||||||
<Select value={numberOfDeployers} onValueChange={setNumberOfDeployers}>
|
|
||||||
<SelectTrigger id="deployers" className={isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}>
|
|
||||||
<SelectValue placeholder="Select number" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="1">1</SelectItem>
|
|
||||||
<SelectItem value="2">2</SelectItem>
|
|
||||||
<SelectItem value="3">3</SelectItem>
|
|
||||||
<SelectItem value="5">5</SelectItem>
|
|
||||||
<SelectItem value="10">10</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex justify-between">
|
||||||
<Label htmlFor="maxPrice" className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}>
|
<span className="text-muted-foreground">Source:</span>
|
||||||
Maximum Price (aint)
|
<span className="font-mono text-xs">{selectedItem}</span>
|
||||||
</Label>
|
</div>
|
||||||
<Select value={maxPrice} onValueChange={setMaxPrice}>
|
<div className="flex justify-between">
|
||||||
<SelectTrigger id="maxPrice" className={isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}>
|
<span className="text-muted-foreground">Project Name:</span>
|
||||||
<SelectValue placeholder="Select price" />
|
<span>{formData.projectName}</span>
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="500">500</SelectItem>
|
|
||||||
<SelectItem value="1000">1000</SelectItem>
|
|
||||||
<SelectItem value="2000">2000</SelectItem>
|
|
||||||
<SelectItem value="5000">5000</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Organization Selection */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<Label htmlFor="organization" className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}>
|
||||||
|
Organization *
|
||||||
|
</Label>
|
||||||
|
{isLoadingOrgs ? (
|
||||||
|
<div className="flex items-center justify-center p-3 border rounded-md">
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||||
|
<span className="text-sm text-muted-foreground">Loading organizations...</span>
|
||||||
|
</div>
|
||||||
|
) : organizations.length === 0 ? (
|
||||||
|
<Alert>
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
No organizations found. You need to be part of at least one organization.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<Select value={selectedOrg} onValueChange={setSelectedOrg}>
|
||||||
|
<SelectTrigger id="organization" className={isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}>
|
||||||
|
<SelectValue placeholder="Select organization" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{organizations.map((org) => (
|
||||||
|
<SelectItem key={org.id} value={org.slug}>
|
||||||
|
{org.name} ({org.slug})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Deployment options */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<Label className={`text-sm mb-3 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}>
|
||||||
|
Deployment Type
|
||||||
|
</Label>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<Button
|
||||||
|
variant={deployOption === 'lrn' ? "default" : "outline"}
|
||||||
|
className={`py-3 ${deployOption === 'lrn'
|
||||||
|
? (isDarkMode ? 'bg-zinc-800 text-white' : 'bg-zinc-800 text-white')
|
||||||
|
: (isDarkMode ? 'bg-transparent border-zinc-700 text-zinc-400' : 'bg-transparent border-zinc-300 text-zinc-600')}`}
|
||||||
|
onClick={() => toggleDeployOption('lrn')}
|
||||||
|
>
|
||||||
|
Deployer LRN
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={deployOption === 'auction' ? "default" : "outline"}
|
||||||
|
className={`py-3 ${deployOption === 'auction'
|
||||||
|
? (isDarkMode ? 'bg-zinc-800 text-white' : 'bg-zinc-800 text-white')
|
||||||
|
: (isDarkMode ? 'bg-transparent border-zinc-700 text-zinc-400' : 'bg-transparent border-zinc-300 text-zinc-600')}`}
|
||||||
|
onClick={() => toggleDeployOption('auction')}
|
||||||
|
>
|
||||||
|
Create Auction
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{deployOption === 'lrn' ? (
|
||||||
|
/* LRN Deployment Settings */
|
||||||
|
<div className="mb-6">
|
||||||
|
<Label htmlFor="lrn" className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}>
|
||||||
|
Select Deployer LRN *
|
||||||
|
</Label>
|
||||||
|
{isLoadingDeployers ? (
|
||||||
|
<div className="flex items-center justify-center p-3 border rounded-md">
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||||
|
<span className="text-sm text-muted-foreground">Loading deployers...</span>
|
||||||
|
</div>
|
||||||
|
) : deployers.length === 0 ? (
|
||||||
|
<Alert>
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
No deployers available. Please contact support.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Select value={selectedLrn} onValueChange={setSelectedLrn}>
|
||||||
|
<SelectTrigger id="lrn" className={isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}>
|
||||||
|
<SelectValue placeholder="Select a deployer" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{deployers.map((deployer) => (
|
||||||
|
<SelectItem key={deployer.deployerLrn} value={deployer.deployerLrn}>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span>{deployer.deployerLrn}</span>
|
||||||
|
{deployer.minimumPayment && (
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
Min payment: {deployer.minimumPayment}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{/* Deployer Details */}
|
||||||
|
{selectedDeployer && (
|
||||||
|
<div className="mt-3 p-3 bg-muted rounded-md">
|
||||||
|
<div className="text-sm space-y-1">
|
||||||
|
<div><strong>API URL:</strong> {selectedDeployer.deployerApiUrl}</div>
|
||||||
|
<div><strong>Base Domain:</strong> {selectedDeployer.baseDomain}</div>
|
||||||
|
{selectedDeployer.minimumPayment && (
|
||||||
|
<div><strong>Minimum Payment:</strong> {selectedDeployer.minimumPayment}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
/* Auction Settings */
|
||||||
{/* LRN settings */}
|
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||||
<div className="mb-6">
|
<div>
|
||||||
<Label htmlFor="lrn" className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}>
|
<Label htmlFor="deployers" className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}>
|
||||||
Select Deployer LRN
|
Number of Deployers
|
||||||
</Label>
|
</Label>
|
||||||
<Select value={selectedLrn} onValueChange={setSelectedLrn}>
|
<Select value={numberOfDeployers} onValueChange={setNumberOfDeployers}>
|
||||||
<SelectTrigger id="lrn" className={isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}>
|
<SelectTrigger id="deployers" className={isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}>
|
||||||
<SelectValue placeholder="Select" />
|
<SelectValue placeholder="Select number" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="lrn-1">Deployer LRN 1</SelectItem>
|
<SelectItem value="1">1</SelectItem>
|
||||||
<SelectItem value="lrn-2">Deployer LRN 2</SelectItem>
|
<SelectItem value="2">2</SelectItem>
|
||||||
<SelectItem value="lrn-3">Deployer LRN 3</SelectItem>
|
<SelectItem value="3">3</SelectItem>
|
||||||
|
<SelectItem value="5">5</SelectItem>
|
||||||
|
<SelectItem value="10">10</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</>
|
<div>
|
||||||
|
<Label htmlFor="maxPrice" className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}>
|
||||||
|
Maximum Price (aint)
|
||||||
|
</Label>
|
||||||
|
<Select value={maxPrice} onValueChange={setMaxPrice}>
|
||||||
|
<SelectTrigger id="maxPrice" className={isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}>
|
||||||
|
<SelectValue placeholder="Select price" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="500">500</SelectItem>
|
||||||
|
<SelectItem value="1000">1000</SelectItem>
|
||||||
|
<SelectItem value="2000">2000</SelectItem>
|
||||||
|
<SelectItem value="5000">5000</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Payment Address */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<Label className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}>
|
||||||
|
Payment Address
|
||||||
|
</Label>
|
||||||
|
<div className={`p-3 border rounded-md bg-muted ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}>
|
||||||
|
<div className="text-sm font-mono break-all">
|
||||||
|
{wallet?.address || 'No wallet connected'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Environment Variables */}
|
{/* Environment Variables */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<Label className={`text-sm font-medium mb-2 block ${isDarkMode ? 'text-white' : 'text-zinc-900'}`}>Environment Variables</Label>
|
<Label className={`text-sm font-medium mb-2 block ${isDarkMode ? 'text-white' : 'text-zinc-900'}`}>
|
||||||
|
Environment Variables
|
||||||
|
</Label>
|
||||||
<div className={`border rounded-md p-4 ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}>
|
<div className={`border rounded-md p-4 ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}>
|
||||||
{envVars.map((envVar, index) => (
|
{envVars.map((envVar, index) => (
|
||||||
<div key={index} className="grid grid-cols-2 gap-2 mb-2">
|
<div key={index} className="space-y-2 mb-4 pb-4 border-b border-muted last:border-b-0 last:mb-0 last:pb-0">
|
||||||
<Input
|
<div className="grid grid-cols-2 gap-2">
|
||||||
placeholder="KEY"
|
<Input
|
||||||
value={envVar.key}
|
placeholder="KEY"
|
||||||
onChange={(e) => {
|
value={envVar.key}
|
||||||
const newEnvVars = [...envVars];
|
onChange={(e) => updateEnvVar(index, 'key', e.target.value)}
|
||||||
newEnvVars[index].key = e.target.value;
|
className={`bg-transparent ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}
|
||||||
setEnvVars(newEnvVars);
|
/>
|
||||||
}}
|
<Input
|
||||||
className={`bg-transparent ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}
|
placeholder="VALUE"
|
||||||
/>
|
value={envVar.value}
|
||||||
<Input
|
onChange={(e) => updateEnvVar(index, 'value', e.target.value)}
|
||||||
placeholder="VALUE"
|
className={`bg-transparent ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}
|
||||||
value={envVar.value}
|
/>
|
||||||
onChange={(e) => {
|
</div>
|
||||||
const newEnvVars = [...envVars];
|
<div className="flex items-center gap-4">
|
||||||
newEnvVars[index].value = e.target.value;
|
<span className="text-xs text-muted-foreground">Environments:</span>
|
||||||
setEnvVars(newEnvVars);
|
{['Production', 'Preview', 'Development'].map((env) => (
|
||||||
}}
|
<div key={env} className="flex items-center gap-1">
|
||||||
className={`bg-transparent ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}
|
<Checkbox
|
||||||
/>
|
id={`${index}-${env}`}
|
||||||
|
checked={envVar.environments.includes(env)}
|
||||||
|
onCheckedChange={() => toggleEnvironment(index, env)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor={`${index}-${env}`} className="text-xs">
|
||||||
|
{env}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{envVars.length > 1 && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => removeEnvVar(index)}
|
||||||
|
className="ml-auto text-red-500 hover:text-red-700 h-6 px-2"
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<Button
|
<Button
|
||||||
@ -241,62 +486,6 @@ export function ConfigureStep() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Environment Tags */}
|
|
||||||
<div className="mb-6">
|
|
||||||
<Label className={`text-sm font-medium mb-2 block ${isDarkMode ? 'text-white' : 'text-zinc-900'}`}>Environment</Label>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Checkbox
|
|
||||||
id="production"
|
|
||||||
checked={environments.production}
|
|
||||||
onCheckedChange={() => toggleEnvironment('production')}
|
|
||||||
className={isDarkMode ? 'border-zinc-600' : 'border-zinc-300'}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="production" className={`ml-2 ${isDarkMode ? 'text-zinc-400' : 'text-zinc-600'}`}>
|
|
||||||
Production
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Checkbox
|
|
||||||
id="preview"
|
|
||||||
checked={environments.preview}
|
|
||||||
onCheckedChange={() => toggleEnvironment('preview')}
|
|
||||||
className={isDarkMode ? 'border-zinc-600' : 'border-zinc-300'}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="preview" className={`ml-2 ${isDarkMode ? 'text-zinc-400' : 'text-zinc-600'}`}>
|
|
||||||
Preview
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Checkbox
|
|
||||||
id="development"
|
|
||||||
checked={environments.development}
|
|
||||||
onCheckedChange={() => toggleEnvironment('development')}
|
|
||||||
className={isDarkMode ? 'border-zinc-600' : 'border-zinc-300'}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="development" className={`ml-2 ${isDarkMode ? 'text-zinc-400' : 'text-zinc-600'}`}>
|
|
||||||
Development
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Account selection */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<Label htmlFor="account" className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}>
|
|
||||||
Select Account
|
|
||||||
</Label>
|
|
||||||
<Select value={selectedAccount} onValueChange={setSelectedAccount}>
|
|
||||||
<SelectTrigger id="account" className={isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}>
|
|
||||||
<SelectValue placeholder="Select" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="account-1">Account 1</SelectItem>
|
|
||||||
<SelectItem value="account-2">Account 2</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Navigation buttons */}
|
{/* Navigation buttons */}
|
||||||
<div className="flex justify-between items-center mt-4">
|
<div className="flex justify-between items-center mt-4">
|
||||||
<Button
|
<Button
|
||||||
@ -310,6 +499,7 @@ export function ConfigureStep() {
|
|||||||
variant="default"
|
variant="default"
|
||||||
className={`${isDarkMode ? 'bg-zinc-800' : 'bg-zinc-800'} text-white hover:bg-zinc-700`}
|
className={`${isDarkMode ? 'bg-zinc-800' : 'bg-zinc-800'} text-white hover:bg-zinc-700`}
|
||||||
onClick={handleNext}
|
onClick={handleNext}
|
||||||
|
disabled={!canProceed()}
|
||||||
>
|
>
|
||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -1,169 +1,449 @@
|
|||||||
|
// src/components/onboarding/connect-step/connect-step.tsx
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Github } from 'lucide-react'
|
import { Github, Wallet, CheckCircle2, AlertTriangle, Loader2, ExternalLink, ChevronDown } from 'lucide-react'
|
||||||
import { useTheme } from 'next-themes'
|
import { useTheme } from 'next-themes'
|
||||||
|
import { SignIn } from '@clerk/nextjs'
|
||||||
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
||||||
import { Button } from '@workspace/ui/components/button'
|
import { useAuthStatus } from '@/hooks/useAuthStatus'
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@workspace/ui/components/select'
|
|
||||||
import { useRepoData } from '@/hooks/useRepoData'
|
import { useRepoData } from '@/hooks/useRepoData'
|
||||||
|
import { Button } from '@workspace/ui/components/button'
|
||||||
|
import { Card, CardContent } from '@workspace/ui/components/card'
|
||||||
|
import { Input } from '@workspace/ui/components/input'
|
||||||
|
import { Label } from '@workspace/ui/components/label'
|
||||||
|
import { Alert, AlertDescription } from '@workspace/ui/components/alert'
|
||||||
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@workspace/ui/components/collapsible'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
import { GitHubBackendAuth } from '@/components/GitHubBackendAuth'
|
||||||
|
import { AVAILABLE_TEMPLATES, type TemplateDetail } from '@/constants/templates'
|
||||||
|
|
||||||
interface Repository {
|
interface Repository {
|
||||||
id: string | number
|
id: string | number
|
||||||
full_name: string
|
full_name: string
|
||||||
html_url?: string
|
html_url?: string
|
||||||
|
description?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConnectStep() {
|
export function ConnectStep() {
|
||||||
const { nextStep, setFormData, formData } = useOnboarding()
|
const { nextStep, setFormData, formData } = useOnboarding()
|
||||||
const { resolvedTheme } = useTheme()
|
const { resolvedTheme } = useTheme()
|
||||||
const [mounted, setMounted] = useState(false)
|
const [mounted, setMounted] = useState(false)
|
||||||
|
|
||||||
|
// Repository vs Template selection
|
||||||
const [selectedRepo, setSelectedRepo] = useState<string>(formData.githubRepo || '')
|
const [selectedRepo, setSelectedRepo] = useState<string>(formData.githubRepo || '')
|
||||||
|
const [selectedTemplate, setSelectedTemplate] = useState<TemplateDetail | undefined>(
|
||||||
|
formData.template || undefined
|
||||||
|
)
|
||||||
|
const [projectName, setProjectName] = useState<string>(formData.projectName || '')
|
||||||
const [isImportMode, setIsImportMode] = useState(true)
|
const [isImportMode, setIsImportMode] = useState(true)
|
||||||
const { repoData: repositories, isLoading } = useRepoData('')
|
|
||||||
|
// Auth status and warning display
|
||||||
|
const [showAuthWarning, setShowAuthWarning] = useState(false)
|
||||||
|
|
||||||
|
// Auth status hook
|
||||||
|
const {
|
||||||
|
clerk,
|
||||||
|
wallet,
|
||||||
|
backend,
|
||||||
|
isFullyAuthenticated,
|
||||||
|
isReady,
|
||||||
|
missing,
|
||||||
|
progress,
|
||||||
|
connectWallet,
|
||||||
|
checkGithubBackendAuth
|
||||||
|
} = useAuthStatus()
|
||||||
|
|
||||||
|
// Repository data
|
||||||
|
const { repoData: repositories, isLoading: isLoadingRepos } = useRepoData('')
|
||||||
|
|
||||||
// Handle hydration mismatch by waiting for mount
|
// Handle hydration mismatch by waiting for mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true)
|
setMounted(true)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Auto-hide auth warning when fully authenticated
|
||||||
|
useEffect(() => {
|
||||||
|
if (isFullyAuthenticated) {
|
||||||
|
setShowAuthWarning(false)
|
||||||
|
}
|
||||||
|
}, [isFullyAuthenticated])
|
||||||
|
|
||||||
// Handle repository selection
|
// Handle repository selection
|
||||||
const handleRepoSelect = (repo: string) => {
|
const handleRepoSelect = (repo: string) => {
|
||||||
setSelectedRepo(repo)
|
setSelectedRepo(repo)
|
||||||
setFormData({ githubRepo: repo })
|
setSelectedTemplate(undefined)
|
||||||
|
setFormData({
|
||||||
|
githubRepo: repo,
|
||||||
|
template: undefined,
|
||||||
|
deploymentMode: 'repository',
|
||||||
|
projectName
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle template selection
|
||||||
|
const handleTemplateSelect = (template: TemplateDetail) => {
|
||||||
|
setSelectedTemplate(template)
|
||||||
|
setSelectedRepo('')
|
||||||
|
// Auto-fill project name if empty
|
||||||
|
if (!projectName) {
|
||||||
|
const suggestedName = `my-${template.name.toLowerCase().replace(/[^a-z0-9]/g, '-')}`
|
||||||
|
setProjectName(suggestedName)
|
||||||
|
}
|
||||||
|
setFormData({
|
||||||
|
template: template,
|
||||||
|
githubRepo: '',
|
||||||
|
deploymentMode: 'template',
|
||||||
|
projectName: projectName || `my-${template.name.toLowerCase().replace(/[^a-z0-9]/g, '-')}`
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle mode toggle between import and template
|
// Handle mode toggle between import and template
|
||||||
const toggleMode = (mode: 'import' | 'template') => {
|
const toggleMode = (mode: 'import' | 'template') => {
|
||||||
setIsImportMode(mode === 'import')
|
setIsImportMode(mode === 'import')
|
||||||
}
|
// Clear selections when switching modes
|
||||||
|
if (mode === 'import') {
|
||||||
// Handle next step
|
setSelectedTemplate(undefined)
|
||||||
const handleNext = () => {
|
setFormData({
|
||||||
if (selectedRepo || !isImportMode) {
|
template: undefined,
|
||||||
nextStep()
|
deploymentMode: 'repository',
|
||||||
|
projectName
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setSelectedRepo('')
|
||||||
|
setFormData({
|
||||||
|
githubRepo: '',
|
||||||
|
deploymentMode: 'template',
|
||||||
|
projectName
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle project name change
|
||||||
|
const handleProjectNameChange = (value: string) => {
|
||||||
|
setProjectName(value)
|
||||||
|
setFormData({ projectName: value })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle wallet connection
|
||||||
|
const handleConnectWallet = async () => {
|
||||||
|
try {
|
||||||
|
await connectWallet()
|
||||||
|
toast.success('Wallet connected successfully')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Wallet connection failed:', error)
|
||||||
|
toast.error('Failed to connect wallet')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle GitHub backend auth status change
|
||||||
|
const handleGithubAuthChange = async (isAuthenticated: boolean) => {
|
||||||
|
await checkGithubBackendAuth()
|
||||||
|
if (isAuthenticated) {
|
||||||
|
toast.success('GitHub backend authentication completed!')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle next step
|
||||||
|
const handleNext = () => {
|
||||||
|
if (!isFullyAuthenticated) {
|
||||||
|
toast.error('Please complete all authentication steps first')
|
||||||
|
setShowAuthWarning(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isImportMode && !selectedRepo) {
|
||||||
|
toast.error('Please select a repository to continue')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isImportMode && (!selectedTemplate || !projectName.trim())) {
|
||||||
|
toast.error('Please select a template and enter a project name')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// For repository import, project name is optional but we'll use repo name as fallback
|
||||||
|
const finalProjectName = projectName.trim() || (isImportMode ? selectedRepo.split('/')[1] : '')
|
||||||
|
|
||||||
|
// Set final form data and proceed
|
||||||
|
setFormData({
|
||||||
|
deploymentMode: isImportMode ? 'repository' : 'template',
|
||||||
|
githubRepo: isImportMode ? selectedRepo : '',
|
||||||
|
template: !isImportMode ? selectedTemplate : undefined,
|
||||||
|
projectName: finalProjectName
|
||||||
|
})
|
||||||
|
|
||||||
|
nextStep()
|
||||||
|
}
|
||||||
|
|
||||||
// Don't render UI until after mount to prevent hydration mismatch
|
// Don't render UI until after mount to prevent hydration mismatch
|
||||||
if (!mounted) {
|
if (!mounted || !isReady) {
|
||||||
return null
|
return (
|
||||||
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin mx-auto mb-4" />
|
||||||
|
<p className="text-sm text-zinc-500">Loading authentication status...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if dark mode is active
|
// Determine if dark mode is active
|
||||||
const isDarkMode = resolvedTheme === 'dark'
|
const isDarkMode = resolvedTheme === 'dark'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full flex flex-col items-center justify-center p-8">
|
<div className="w-full h-full flex flex-col p-8 overflow-y-auto">
|
||||||
<div className="max-w-md w-full mx-auto">
|
<div className="max-w-2xl w-full mx-auto">
|
||||||
{/* Connect icon */}
|
{/* Header */}
|
||||||
<div className="mx-auto mb-6 flex justify-center">
|
<div className="text-center mb-8">
|
||||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={isDarkMode ? "text-white" : "text-black"}>
|
<h2 className={`text-2xl font-medium ${isDarkMode ? "text-white" : "text-zinc-900"} mb-2`}>
|
||||||
<path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
Connect
|
||||||
<line x1="8" y1="12" x2="16" y2="12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
</h2>
|
||||||
</svg>
|
<p className="text-zinc-500 mb-6">
|
||||||
</div>
|
Connect and import a GitHub repo or start from a template
|
||||||
|
</p>
|
||||||
{/* Connect header */}
|
|
||||||
<h2 className={`text-2xl font-medium ${isDarkMode ? "text-white" : "text-zinc-900"} text-center mb-2`}>Connect</h2>
|
{/* GitHub Account Selector - Only show if multiple accounts */}
|
||||||
<p className="text-center text-zinc-500 mb-8">
|
{clerk.user?.externalAccounts && clerk.user.externalAccounts.length > 1 && (
|
||||||
Connect and import a GitHub repo or start from a template
|
<div className="flex items-center justify-center mb-6">
|
||||||
</p>
|
<div className="flex items-center gap-2 px-4 py-2 bg-zinc-100 dark:bg-zinc-800 rounded-md cursor-pointer hover:bg-zinc-200 dark:hover:bg-zinc-700">
|
||||||
|
<Github className="h-4 w-4" />
|
||||||
{/* Git account selector */}
|
<span className="text-sm font-medium">
|
||||||
<div className="mb-4">
|
{clerk.user?.externalAccounts?.find(acc => acc.provider === 'github')?.username || 'git-account'}
|
||||||
<Select defaultValue="git-account">
|
</span>
|
||||||
<SelectTrigger className={`w-full py-3 ${isDarkMode ? "border-zinc-700" : "border-zinc-300"}`}>
|
<ChevronDown className="h-4 w-4" />
|
||||||
<div className="flex items-center">
|
|
||||||
<Github className="mr-2 h-5 w-5" />
|
|
||||||
<SelectValue placeholder="Select Git account" />
|
|
||||||
</div>
|
</div>
|
||||||
</SelectTrigger>
|
</div>
|
||||||
<SelectContent>
|
)}
|
||||||
<SelectItem value="git-account">git-account</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mode buttons */}
|
{/* Authentication Warning - Only show if not fully authenticated */}
|
||||||
<div className="grid grid-cols-2 gap-2 mb-4">
|
{!isFullyAuthenticated && (
|
||||||
|
<Collapsible open={showAuthWarning} onOpenChange={setShowAuthWarning}>
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<Alert className="mb-6 cursor-pointer hover:bg-amber-50 dark:hover:bg-amber-950/20">
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
<AlertDescription className="flex items-center justify-between w-full">
|
||||||
|
<span>Authentication required to continue ({progress.completed}/{progress.total} complete)</span>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent className="space-y-4 mb-6">
|
||||||
|
{/* Authentication steps - same as before but in collapsible */}
|
||||||
|
{missing.clerkSignIn && (
|
||||||
|
<Card className="border-amber-200 bg-amber-50/50 dark:bg-amber-950/20">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-3 mb-3">
|
||||||
|
<Github className="h-4 w-4" />
|
||||||
|
<span className="text-sm font-medium">Sign in with Clerk</span>
|
||||||
|
</div>
|
||||||
|
<div className="scale-90 origin-top-left">
|
||||||
|
<SignIn routing="hash" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{missing.clerkGithub && !missing.clerkSignIn && (
|
||||||
|
<Card className="border-amber-200 bg-amber-50/50 dark:bg-amber-950/20">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-3 mb-3">
|
||||||
|
<Github className="h-4 w-4" />
|
||||||
|
<span className="text-sm font-medium">Connect GitHub Account</span>
|
||||||
|
</div>
|
||||||
|
<Button size="sm" variant="outline" onClick={() => window.open('/user-profile', '_blank')}>
|
||||||
|
<ExternalLink className="h-3 w-3 mr-2" />
|
||||||
|
Connect GitHub
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{missing.walletConnection && (
|
||||||
|
<Card className="border-amber-200 bg-amber-50/50 dark:bg-amber-950/20">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-3 mb-3">
|
||||||
|
<Wallet className="h-4 w-4" />
|
||||||
|
<span className="text-sm font-medium">Connect Wallet</span>
|
||||||
|
</div>
|
||||||
|
<Button size="sm" onClick={handleConnectWallet}>
|
||||||
|
Connect Wallet
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{missing.githubBackendSync && !missing.walletConnection && !missing.clerkGithub && (
|
||||||
|
<Card className="border-amber-200 bg-amber-50/50 dark:bg-amber-950/20">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-3 mb-3">
|
||||||
|
<Github className="h-4 w-4" />
|
||||||
|
<span className="text-sm font-medium">Sync GitHub Access</span>
|
||||||
|
</div>
|
||||||
|
<GitHubBackendAuth onAuthStatusChange={handleGithubAuthChange} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Mode Selection Tabs */}
|
||||||
|
<div className="grid grid-cols-2 gap-1 p-1 bg-zinc-100 dark:bg-zinc-800 rounded-lg mb-6">
|
||||||
<Button
|
<Button
|
||||||
variant={isImportMode ? "default" : "outline"}
|
variant={isImportMode ? "default" : "ghost"}
|
||||||
className={`py-3 ${isImportMode
|
className={`${isImportMode
|
||||||
? (isDarkMode ? 'bg-zinc-800 text-white' : 'bg-zinc-800 text-white')
|
? 'bg-white dark:bg-zinc-700 shadow-sm'
|
||||||
: (isDarkMode ? 'bg-transparent border-zinc-700 text-zinc-400' : 'bg-transparent border-zinc-300 text-zinc-600')}`}
|
: 'bg-transparent hover:bg-white/50 dark:hover:bg-zinc-700/50'
|
||||||
|
}`}
|
||||||
onClick={() => toggleMode('import')}
|
onClick={() => toggleMode('import')}
|
||||||
>
|
>
|
||||||
Import a repository
|
Import a repository
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={!isImportMode ? "default" : "outline"}
|
variant={!isImportMode ? "default" : "ghost"}
|
||||||
className={`py-3 ${!isImportMode
|
className={`${!isImportMode
|
||||||
? (isDarkMode ? 'bg-zinc-800 text-white' : 'bg-zinc-800 text-white')
|
? 'bg-white dark:bg-zinc-700 shadow-sm'
|
||||||
: (isDarkMode ? 'bg-transparent border-zinc-700 text-zinc-400' : 'bg-transparent border-zinc-300 text-zinc-600')}`}
|
: 'bg-transparent hover:bg-white/50 dark:hover:bg-zinc-700/50'
|
||||||
|
}`}
|
||||||
onClick={() => toggleMode('template')}
|
onClick={() => toggleMode('template')}
|
||||||
>
|
>
|
||||||
Start with a template
|
Start with a template
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Repository or template list */}
|
{/* Content Area */}
|
||||||
{isImportMode ? (
|
{isImportMode ? (
|
||||||
<div className={`border rounded-md overflow-hidden mb-4 ${isDarkMode ? "border-zinc-700" : "border-zinc-300"}`}>
|
/* Repository Selection */
|
||||||
{isLoading ? (
|
<div className="space-y-4">
|
||||||
<div className="p-6 text-center text-zinc-500">
|
{isLoadingRepos ? (
|
||||||
|
<div className="p-8 text-center text-zinc-500">
|
||||||
<div className="animate-spin h-5 w-5 border-2 border-zinc-500 border-t-transparent rounded-full mx-auto mb-2"></div>
|
<div className="animate-spin h-5 w-5 border-2 border-zinc-500 border-t-transparent rounded-full mx-auto mb-2"></div>
|
||||||
Loading repositories...
|
Loading repositories...
|
||||||
</div>
|
</div>
|
||||||
) : !repositories || repositories.length === 0 ? (
|
) : !repositories || repositories.length === 0 ? (
|
||||||
<div className="p-6 text-center text-zinc-500">
|
<div className="p-8 text-center text-zinc-500">
|
||||||
No repositories found
|
<Alert>
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
No repositories found. Make sure your GitHub account has repositories.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="max-h-60 overflow-y-auto">
|
<>
|
||||||
{repositories.map((repo: Repository) => (
|
<div className="space-y-2 max-h-60 overflow-y-auto">
|
||||||
<div
|
{repositories.map((repo: Repository) => (
|
||||||
key={repo.id}
|
<div
|
||||||
className={`flex items-center p-4 ${isDarkMode ? "border-zinc-700" : "border-zinc-200"} border-b last:border-b-0 cursor-pointer ${
|
key={repo.id}
|
||||||
selectedRepo === repo.full_name
|
className={`flex items-center p-4 rounded-lg border cursor-pointer transition-all ${
|
||||||
? (isDarkMode ? 'bg-zinc-800' : 'bg-zinc-100')
|
selectedRepo === repo.full_name
|
||||||
: (isDarkMode ? 'hover:bg-zinc-800' : 'hover:bg-zinc-50')
|
? 'border-blue-500 bg-blue-50 dark:bg-blue-950/20'
|
||||||
}`}
|
: 'border-zinc-200 dark:border-zinc-700 hover:border-zinc-300 dark:hover:border-zinc-600'
|
||||||
onClick={() => handleRepoSelect(repo.full_name)}
|
}`}
|
||||||
>
|
onClick={() => handleRepoSelect(repo.full_name)}
|
||||||
<div className={`flex-1 ${isDarkMode ? "text-white" : "text-zinc-900"}`}>
|
>
|
||||||
<Github className="inline-block h-4 w-4 mr-2 text-zinc-500" />
|
<Github className="h-5 w-5 mr-3 text-zinc-500 flex-shrink-0" />
|
||||||
<span>{repo.full_name}</span>
|
<div className="flex-1 min-w-0">
|
||||||
</div>
|
<div className="font-medium text-sm">{repo.full_name}</div>
|
||||||
<div className="text-sm text-zinc-500">
|
{repo.description && (
|
||||||
5 minutes ago
|
<div className="text-xs text-zinc-500 truncate">{repo.description}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{selectedRepo === repo.full_name && (
|
||||||
|
<CheckCircle2 className="h-5 w-5 text-blue-500 flex-shrink-0" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Project Name Input for Repository Import */}
|
||||||
|
{selectedRepo && (
|
||||||
|
<div className="mt-6 space-y-2">
|
||||||
|
<Label htmlFor="projectName" className="text-sm font-medium">
|
||||||
|
Project Name
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="projectName"
|
||||||
|
value={projectName}
|
||||||
|
onChange={(e) => handleProjectNameChange(e.target.value)}
|
||||||
|
placeholder="my-project-name"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-zinc-500">
|
||||||
|
This will be the name of your deployment project
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={`border rounded-md overflow-hidden mb-4 ${isDarkMode ? "border-zinc-700" : "border-zinc-300"}`}>
|
/* Template Selection */
|
||||||
<div className="p-6 text-center text-zinc-500">
|
<div className="space-y-4">
|
||||||
Template selection coming soon
|
{AVAILABLE_TEMPLATES.filter(t => !t.isComingSoon).map((template) => (
|
||||||
</div>
|
<div
|
||||||
|
key={template.id}
|
||||||
|
className={`flex items-center p-4 rounded-lg border cursor-pointer transition-all ${
|
||||||
|
selectedTemplate?.id === template.id
|
||||||
|
? 'border-blue-500 bg-blue-50 dark:bg-blue-950/20'
|
||||||
|
: 'border-zinc-200 dark:border-zinc-700 hover:border-zinc-300 dark:hover:border-zinc-600'
|
||||||
|
}`}
|
||||||
|
onClick={() => handleTemplateSelect(template)}
|
||||||
|
>
|
||||||
|
{/* Template Icon */}
|
||||||
|
<div className="flex items-center justify-center w-10 h-10 rounded-lg bg-zinc-100 dark:bg-zinc-800 mr-4">
|
||||||
|
<div className="w-6 h-6 bg-zinc-600 dark:bg-zinc-400 rounded flex items-center justify-center text-xs font-bold text-white">
|
||||||
|
{template.icon === 'web' ? 'PWA' : template.icon === 'nextjs' ? 'N' : 'IMG'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Template Info */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="font-medium text-sm mb-1">{template.name}</div>
|
||||||
|
<div className="flex items-center text-xs text-zinc-500">
|
||||||
|
<Github className="h-3 w-3 mr-1" />
|
||||||
|
{template.repoFullName}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Selection Indicator */}
|
||||||
|
{selectedTemplate?.id === template.id && (
|
||||||
|
<CheckCircle2 className="h-5 w-5 text-blue-500 flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Project Name Input for Templates */}
|
||||||
|
{selectedTemplate && (
|
||||||
|
<div className="mt-6 space-y-2">
|
||||||
|
<Label htmlFor="projectName" className="text-sm font-medium">
|
||||||
|
Project Name
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="projectName"
|
||||||
|
value={projectName}
|
||||||
|
onChange={(e) => handleProjectNameChange(e.target.value)}
|
||||||
|
placeholder="new-repository-name"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-zinc-500">
|
||||||
|
This will be the name of your new GitHub repository
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Navigation buttons */}
|
{/* Navigation */}
|
||||||
<div className="flex justify-between items-center mt-8">
|
<div className="flex justify-between items-center mt-8">
|
||||||
<Button
|
<Button variant="outline" disabled>
|
||||||
variant="outline"
|
|
||||||
className={`${isDarkMode ? "text-zinc-400 border-zinc-700" : "text-zinc-600 border-zinc-300"} bg-transparent`}
|
|
||||||
disabled={true}
|
|
||||||
>
|
|
||||||
Previous
|
Previous
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className={`${isDarkMode ? 'bg-zinc-800' : 'bg-zinc-800'} text-white hover:bg-zinc-700`}
|
|
||||||
onClick={handleNext}
|
onClick={handleNext}
|
||||||
disabled={!selectedRepo && isImportMode}
|
disabled={!isFullyAuthenticated || (isImportMode ? !selectedRepo : (!selectedTemplate || !projectName.trim()))}
|
||||||
>
|
>
|
||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -1,12 +1,21 @@
|
|||||||
|
// src/components/onboarding/deploy-step/deploy-step.tsx
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useTheme } from 'next-themes'
|
import { useTheme } from 'next-themes'
|
||||||
import { Github, Loader2 } from 'lucide-react'
|
import { Github, Loader2, AlertTriangle, CheckCircle2 } from 'lucide-react'
|
||||||
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
||||||
|
import { useGQLClient } from '@/context'
|
||||||
|
import { useWallet } from '@/context/WalletContext'
|
||||||
|
import { useDeployment } from '@/hooks/useDeployment'
|
||||||
|
import { useTemplateDeployment } from '@/hooks/useTemplate'
|
||||||
import { Button } from '@workspace/ui/components/button'
|
import { Button } from '@workspace/ui/components/button'
|
||||||
import { Progress } from '@workspace/ui/components/progress'
|
import { Progress } from '@workspace/ui/components/progress'
|
||||||
import { Dialog, DialogContent, DialogTitle, DialogDescription, DialogFooter } from '@workspace/ui/components/dialog'
|
import { Dialog, DialogContent, DialogTitle, DialogDescription, DialogFooter } from '@workspace/ui/components/dialog'
|
||||||
|
import { Alert, AlertDescription } from '@workspace/ui/components/alert'
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card'
|
||||||
|
import { Badge } from '@workspace/ui/components/badge'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
export function DeployStep() {
|
export function DeployStep() {
|
||||||
const { previousStep, nextStep, formData, setFormData } = useOnboarding()
|
const { previousStep, nextStep, formData, setFormData } = useOnboarding()
|
||||||
@ -14,21 +23,67 @@ export function DeployStep() {
|
|||||||
const [mounted, setMounted] = useState(false)
|
const [mounted, setMounted] = useState(false)
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const [isDeploying, setIsDeploying] = useState(false)
|
|
||||||
const [deploymentProgress, setDeploymentProgress] = useState(0)
|
|
||||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false)
|
const [showConfirmDialog, setShowConfirmDialog] = useState(false)
|
||||||
|
const [deploymentError, setDeploymentError] = useState<string | null>(null)
|
||||||
|
const [deploymentSuccess, setDeploymentSuccess] = useState(false)
|
||||||
|
|
||||||
|
// Contexts and hooks
|
||||||
|
const { wallet } = useWallet()
|
||||||
|
const { deployRepository, isDeploying: isRepoDeploying } = useDeployment()
|
||||||
|
const { deployTemplate, isDeploying: isTemplateDeploying } = useTemplateDeployment()
|
||||||
|
|
||||||
|
// Determine deployment type and get the right deploying state
|
||||||
|
const isTemplateMode = formData.deploymentMode === 'template'
|
||||||
|
const isDeploying = isTemplateMode ? isTemplateDeploying : isRepoDeploying
|
||||||
|
|
||||||
// Handle hydration mismatch by waiting for mount
|
// Handle hydration mismatch by waiting for mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true)
|
setMounted(true)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Repository information from previous steps
|
// Get deployment info
|
||||||
const repoFullName = formData.githubRepo || 'git-account/repo-name'
|
const getDeploymentInfo = () => {
|
||||||
const branch = 'main'
|
if (isTemplateMode) {
|
||||||
|
return {
|
||||||
|
name: formData.template?.name || 'Template Project',
|
||||||
|
source: formData.template?.repoFullName || 'Unknown Template',
|
||||||
|
projectName: formData.projectName || 'New Project',
|
||||||
|
type: 'Template'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
name: formData.githubRepo?.split('/').pop() || 'Repository',
|
||||||
|
source: formData.githubRepo || 'Unknown Repository',
|
||||||
|
projectName: formData.projectName || formData.githubRepo?.split('/').pop() || 'New Project',
|
||||||
|
type: 'Repository'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deploymentInfo = getDeploymentInfo()
|
||||||
|
|
||||||
// Open the confirmation modal
|
// Open the confirmation modal
|
||||||
const handlePayAndDeploy = () => {
|
const handlePayAndDeploy = () => {
|
||||||
|
if (!wallet?.address) {
|
||||||
|
toast.error('Wallet not connected')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.selectedOrg) {
|
||||||
|
toast.error('No organization selected')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTemplateMode && (!formData.template || !formData.projectName)) {
|
||||||
|
toast.error('Template or project name missing')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isTemplateMode && !formData.githubRepo) {
|
||||||
|
toast.error('Repository not selected')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setShowConfirmDialog(true)
|
setShowConfirmDialog(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,42 +93,91 @@ export function DeployStep() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle confirmed deployment
|
// Handle confirmed deployment
|
||||||
const handleConfirmDeploy = () => {
|
const handleConfirmDeploy = async () => {
|
||||||
setShowConfirmDialog(false)
|
setShowConfirmDialog(false)
|
||||||
startDeployment()
|
setDeploymentError(null)
|
||||||
|
setDeploymentSuccess(false)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isTemplateMode) {
|
||||||
|
await deployTemplateProject()
|
||||||
|
} else {
|
||||||
|
await deployRepositoryProject()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Deployment failed:', error)
|
||||||
|
setDeploymentError(error instanceof Error ? error.message : 'Deployment failed')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the deployment process
|
// Deploy template project
|
||||||
const startDeployment = () => {
|
const deployTemplateProject = async () => {
|
||||||
setIsDeploying(true)
|
if (!formData.template || !formData.projectName || !formData.selectedOrg) {
|
||||||
|
throw new Error('Missing required template deployment data')
|
||||||
|
}
|
||||||
|
|
||||||
// Simulate deployment process with progress updates
|
const config = {
|
||||||
let progress = 0
|
template: formData.template,
|
||||||
const interval = setInterval(() => {
|
projectName: formData.projectName,
|
||||||
progress += 10
|
organizationSlug: formData.selectedOrg,
|
||||||
setDeploymentProgress(progress)
|
environmentVariables: formData.environmentVariables || [],
|
||||||
|
deployerLrn: formData.selectedLrn
|
||||||
if (progress >= 100) {
|
}
|
||||||
clearInterval(interval)
|
|
||||||
|
console.log('Deploying template with config:', config)
|
||||||
// Generate deployment ID and create URL
|
|
||||||
const deploymentId = `deploy-${Math.random().toString(36).substring(2, 9)}`
|
const result = await deployTemplate(config)
|
||||||
const repoName = repoFullName.split('/').pop() || 'app'
|
|
||||||
const projectId = `proj-${Math.random().toString(36).substring(2, 9)}`
|
// Save deployment results
|
||||||
|
setFormData({
|
||||||
// Save deployment info
|
deploymentId: result.deploymentId,
|
||||||
setFormData({
|
deploymentUrl: result.deploymentUrl,
|
||||||
deploymentId,
|
projectId: result.projectId,
|
||||||
deploymentUrl: `https://${repoName}.laconic.deploy`,
|
repositoryUrl: result.repositoryUrl
|
||||||
projectId
|
})
|
||||||
})
|
|
||||||
|
setDeploymentSuccess(true)
|
||||||
// Move to success step after short delay
|
toast.success('Template deployed successfully!')
|
||||||
setTimeout(() => {
|
|
||||||
nextStep()
|
// Move to success step after short delay
|
||||||
}, 500)
|
setTimeout(() => {
|
||||||
}
|
nextStep()
|
||||||
}, 500)
|
}, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deploy repository project
|
||||||
|
const deployRepositoryProject = async () => {
|
||||||
|
if (!formData.githubRepo || !formData.selectedOrg) {
|
||||||
|
throw new Error('Missing required repository deployment data')
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
projectId: '', // Will be generated by backend
|
||||||
|
organizationSlug: formData.selectedOrg,
|
||||||
|
repository: formData.githubRepo,
|
||||||
|
branch: 'main', // Default branch
|
||||||
|
name: formData.projectName || formData.githubRepo.split('/').pop() || 'New Project',
|
||||||
|
environmentVariables: formData.environmentVariables || []
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Deploying repository with config:', config)
|
||||||
|
|
||||||
|
const result = await deployRepository(config)
|
||||||
|
|
||||||
|
// Save deployment results
|
||||||
|
setFormData({
|
||||||
|
deploymentId: result.id,
|
||||||
|
deploymentUrl: result.url,
|
||||||
|
projectId: result.id
|
||||||
|
})
|
||||||
|
|
||||||
|
setDeploymentSuccess(true)
|
||||||
|
toast.success('Repository deployed successfully!')
|
||||||
|
|
||||||
|
// Move to success step after short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
nextStep()
|
||||||
|
}, 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't render UI until after mount to prevent hydration mismatch
|
// Don't render UI until after mount to prevent hydration mismatch
|
||||||
@ -101,41 +205,92 @@ export function DeployStep() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Deploy header */}
|
{/* Deploy header */}
|
||||||
<h2 className={`text-2xl font-medium ${isDarkMode ? "text-white" : "text-zinc-900"} text-center mb-2`}>Deploy</h2>
|
<h2 className={`text-2xl font-medium ${isDarkMode ? "text-white" : "text-zinc-900"} text-center mb-2`}>
|
||||||
|
{isDeploying ? 'Deploying...' : 'Deploy'}
|
||||||
|
</h2>
|
||||||
<p className="text-center text-zinc-500 mb-8">
|
<p className="text-center text-zinc-500 mb-8">
|
||||||
Your deployment is configured and ready to go!
|
{isDeploying
|
||||||
|
? 'Your project is being deployed. This may take a few minutes.'
|
||||||
|
: 'Review and confirm deployment'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Repository info */}
|
{/* Deployment Summary */}
|
||||||
<div className={`border rounded-lg overflow-hidden mb-8 ${isDarkMode ? "border-zinc-700" : "border-zinc-300"}`}>
|
<Card className="mb-6">
|
||||||
<div className="p-5 flex items-center">
|
<CardHeader className="pb-3">
|
||||||
<div className="mr-3">
|
<CardTitle className="text-sm flex items-center justify-between">
|
||||||
<Github className="h-5 w-5 text-zinc-500" />
|
Deployment Summary
|
||||||
</div>
|
<Badge variant="secondary">{deploymentInfo.type}</Badge>
|
||||||
<div className="flex-1">
|
</CardTitle>
|
||||||
<div className={isDarkMode ? "text-white" : "text-zinc-900"}>{repoFullName}</div>
|
</CardHeader>
|
||||||
<div className="text-sm text-zinc-500">
|
<CardContent className="pt-0 space-y-3">
|
||||||
<svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" className="inline-block mr-1">
|
<div className="flex items-center gap-3">
|
||||||
<polyline points="6 9 12 15 18 9"></polyline>
|
<Github className="h-4 w-4 text-muted-foreground" />
|
||||||
</svg>
|
<div className="flex-1 min-w-0">
|
||||||
{branch}
|
<div className="font-medium text-sm">{deploymentInfo.projectName}</div>
|
||||||
|
<div className="text-xs text-muted-foreground font-mono">
|
||||||
|
{deploymentInfo.source}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
<div className="grid grid-cols-2 gap-4 pt-2 text-xs">
|
||||||
|
<div>
|
||||||
|
<div className="text-muted-foreground">Organization</div>
|
||||||
|
<div className="font-medium">{formData.selectedOrg}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-muted-foreground">Deployer</div>
|
||||||
|
<div className="font-medium">{formData.selectedLrn ? 'LRN' : 'Auction'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{formData.environmentVariables && formData.environmentVariables.length > 0 && (
|
||||||
|
<div className="pt-2">
|
||||||
|
<div className="text-xs text-muted-foreground mb-1">Environment Variables</div>
|
||||||
|
<div className="text-xs">
|
||||||
|
{formData.environmentVariables.length} variable{formData.environmentVariables.length !== 1 ? 's' : ''} configured
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* Deployment progress */}
|
{/* Error Display */}
|
||||||
{isDeploying && deploymentProgress > 0 && (
|
{deploymentError && (
|
||||||
|
<Alert className="mb-6" variant="destructive">
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
<div className="font-medium">Deployment Failed</div>
|
||||||
|
<div className="text-sm mt-1">{deploymentError}</div>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Success Display */}
|
||||||
|
{deploymentSuccess && (
|
||||||
|
<Alert className="mb-6">
|
||||||
|
<CheckCircle2 className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
<div className="font-medium">Deployment Successful!</div>
|
||||||
|
<div className="text-sm mt-1">
|
||||||
|
Your project has been deployed successfully. You'll be redirected to the project dashboard.
|
||||||
|
</div>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Deployment Progress - Only show while deploying */}
|
||||||
|
{isDeploying && (
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex justify-between items-center mb-2">
|
<div className="flex justify-between items-center mb-2">
|
||||||
<div className={`${isDarkMode ? "text-white" : "text-zinc-900"} text-sm`}>
|
<div className={`${isDarkMode ? "text-white" : "text-zinc-900"} text-sm`}>
|
||||||
{deploymentProgress < 30 && "Preparing deployment..."}
|
{isTemplateMode ? 'Creating repository from template...' : 'Deploying repository...'}
|
||||||
{deploymentProgress >= 30 && deploymentProgress < 90 && "Deploying your project..."}
|
|
||||||
{deploymentProgress >= 90 && "Finalizing deployment..."}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-zinc-500 text-xs">{deploymentProgress}%</div>
|
|
||||||
</div>
|
</div>
|
||||||
<Progress value={deploymentProgress} className={`h-1 ${isDarkMode ? "bg-zinc-800" : "bg-zinc-200"}`} />
|
<Progress value={undefined} className={`h-2 ${isDarkMode ? "bg-zinc-800" : "bg-zinc-200"}`} />
|
||||||
|
<div className="text-xs text-muted-foreground mt-2">
|
||||||
|
This process may take several minutes. Please do not close this window.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -145,12 +300,20 @@ export function DeployStep() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className={`${isDarkMode ? "text-zinc-400 border-zinc-700" : "text-zinc-600 border-zinc-300"} bg-transparent`}
|
className={`${isDarkMode ? "text-zinc-400 border-zinc-700" : "text-zinc-600 border-zinc-300"} bg-transparent`}
|
||||||
onClick={previousStep}
|
onClick={previousStep}
|
||||||
disabled={isDeploying}
|
disabled={isDeploying || deploymentSuccess}
|
||||||
>
|
>
|
||||||
Previous
|
Previous
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{isDeploying ? (
|
{deploymentSuccess ? (
|
||||||
|
<Button
|
||||||
|
className="bg-green-600 hover:bg-green-700 text-white"
|
||||||
|
onClick={nextStep}
|
||||||
|
>
|
||||||
|
<CheckCircle2 className="mr-2 h-4 w-4" />
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
) : isDeploying ? (
|
||||||
<Button
|
<Button
|
||||||
className={`${isDarkMode ? "bg-zinc-700 text-zinc-300" : "bg-zinc-300 text-zinc-600"}`}
|
className={`${isDarkMode ? "bg-zinc-700 text-zinc-300" : "bg-zinc-300 text-zinc-600"}`}
|
||||||
disabled
|
disabled
|
||||||
@ -162,8 +325,9 @@ export function DeployStep() {
|
|||||||
<Button
|
<Button
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white flex items-center"
|
className="bg-blue-600 hover:bg-blue-700 text-white flex items-center"
|
||||||
onClick={handlePayAndDeploy}
|
onClick={handlePayAndDeploy}
|
||||||
|
disabled={deploymentError !== null}
|
||||||
>
|
>
|
||||||
Pay and Deploy
|
{formData.deploymentType === 'auction' ? 'Pay and Deploy' : 'Deploy'}
|
||||||
<svg className="ml-2 h-4 w-4" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
<svg className="ml-2 h-4 w-4" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M5 12H19M19 12L13 6M19 12L13 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
<path d="M5 12H19M19 12L13 6M19 12L13 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
@ -175,69 +339,79 @@ export function DeployStep() {
|
|||||||
|
|
||||||
{/* Transaction Confirmation Dialog */}
|
{/* Transaction Confirmation Dialog */}
|
||||||
<Dialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
<Dialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
||||||
<DialogContent className="bg-black border-zinc-800 text-white max-w-md">
|
<DialogContent className="bg-background border max-w-md">
|
||||||
<DialogTitle className="text-white">Confirm Transaction</DialogTitle>
|
<DialogTitle>Confirm Deployment</DialogTitle>
|
||||||
<DialogDescription className="text-zinc-400">
|
<DialogDescription>
|
||||||
This is a dialog description.
|
Review the deployment details before proceeding.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
|
|
||||||
<div className="space-y-6 py-4">
|
<div className="space-y-4 py-4">
|
||||||
{/* From */}
|
{/* Project Info */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h3 className="text-lg font-medium text-white">From</h3>
|
<h3 className="text-sm font-medium">Project Details</h3>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1 text-sm">
|
||||||
<div className="text-sm text-zinc-400">Address</div>
|
<div className="flex justify-between">
|
||||||
<div className="text-sm text-white break-all font-mono">laconic1sdfjwel4jfkasfjgjal45ioasjj5jjlajfjj355</div>
|
<span className="text-muted-foreground">Name:</span>
|
||||||
</div>
|
<span>{deploymentInfo.projectName}</span>
|
||||||
<div className="space-y-1">
|
</div>
|
||||||
<div className="text-sm text-zinc-400">Public Key</div>
|
<div className="flex justify-between">
|
||||||
<div className="text-sm text-white break-all font-mono">laconic1sdfjwel4jfkasfjgjal45ioasjj5jjlajfjj355</div>
|
<span className="text-muted-foreground">Type:</span>
|
||||||
</div>
|
<span>{deploymentInfo.type}</span>
|
||||||
<div className="space-y-1">
|
</div>
|
||||||
<div className="text-sm text-zinc-400">HD Path</div>
|
<div className="flex justify-between">
|
||||||
<div className="text-sm text-white font-mono">m/44/118/0/0/0</div>
|
<span className="text-muted-foreground">Source:</span>
|
||||||
|
<span className="font-mono text-xs">{deploymentInfo.source}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Balance */}
|
{/* Wallet Info */}
|
||||||
<div className="space-y-1">
|
<div className="space-y-2">
|
||||||
<div className="text-lg font-medium text-white">Balance</div>
|
<h3 className="text-sm font-medium">Payment Address</h3>
|
||||||
<div className="text-lg text-white">129600</div>
|
<div className="p-2 bg-muted rounded text-xs font-mono break-all">
|
||||||
</div>
|
{wallet?.address}
|
||||||
|
|
||||||
{/* To */}
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="text-lg font-medium text-white">To</div>
|
|
||||||
<div className="text-sm text-white break-all font-mono">laconic1sdfjwel4jfkasfjgjal45ioasjj5jjlajfjj355</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Amount */}
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="text-lg font-medium text-white">Amount</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="text-sm text-zinc-400">Balance (aint)</div>
|
|
||||||
<div className="text-sm text-white">129600</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="text-sm text-zinc-400">Amount (aint)</div>
|
|
||||||
<div className="text-sm text-white">3000</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Deployer Info */}
|
||||||
|
{formData.selectedLrn && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-sm font-medium">Deployer</h3>
|
||||||
|
<div className="text-sm">
|
||||||
|
<div className="font-mono text-xs">{formData.selectedLrn}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Cost Info */}
|
||||||
|
{formData.deploymentType === 'auction' && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-sm font-medium">Auction Details</h3>
|
||||||
|
<div className="space-y-1 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">Max Price:</span>
|
||||||
|
<span>{formData.maxPrice} aint</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">Deployers:</span>
|
||||||
|
<span>{formData.deployerCount}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="flex justify-end space-x-2">
|
<DialogFooter className="flex justify-end space-x-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="text-zinc-400 bg-zinc-900 border-zinc-800 hover:bg-zinc-800"
|
|
||||||
onClick={handleCancelConfirm}
|
onClick={handleCancelConfirm}
|
||||||
>
|
>
|
||||||
No, cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="bg-white text-black hover:bg-white/90"
|
|
||||||
onClick={handleConfirmDeploy}
|
onClick={handleConfirmDeploy}
|
||||||
>
|
>
|
||||||
Yes, confirm
|
Confirm Deployment
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import { useTheme } from 'next-themes'
|
|||||||
import { CheckCircle } from 'lucide-react'
|
import { CheckCircle } from 'lucide-react'
|
||||||
import { Button } from '@workspace/ui/components/button'
|
import { Button } from '@workspace/ui/components/button'
|
||||||
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
import { useOnboarding } from '@/components/onboarding/useOnboarding'
|
||||||
import { toast } from 'sonner'
|
|
||||||
|
|
||||||
export function SuccessStep() {
|
export function SuccessStep() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -22,20 +21,30 @@ export function SuccessStep() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Get deployment info from form data
|
// Get deployment info from form data
|
||||||
const repoName = formData.githubRepo ? formData.githubRepo.split('/').pop() : 'blogapp'
|
const repoName = formData.githubRepo ? formData.githubRepo.split('/').pop() : (formData.projectName || 'project')
|
||||||
const deploymentUrl = formData.deploymentUrl || `https://${repoName}.laconic.deploy`
|
const deploymentUrl = formData.deploymentUrl || `https://${repoName}.laconic.deploy`
|
||||||
const projectId = formData.projectId || 'unknown-id'
|
const projectId = formData.projectId || 'unknown-id'
|
||||||
|
|
||||||
// Function to copy URL to clipboard
|
|
||||||
|
|
||||||
// Handle "Visit Site" button
|
|
||||||
|
|
||||||
// Handle "View Project" button - navigates to project page
|
// Handle "View Project" button - navigates to project page
|
||||||
const handleViewProject = () => {
|
const handleViewProject = () => {
|
||||||
|
console.log('Navigating to project with ID:', projectId)
|
||||||
resetOnboarding() // Reset state for next time
|
resetOnboarding() // Reset state for next time
|
||||||
|
|
||||||
|
// Navigate to the project detail page using the GraphQL project ID
|
||||||
router.push(`/projects/${providerParam}/ps/${projectId}`)
|
router.push(`/projects/${providerParam}/ps/${projectId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-redirect after a delay (optional)
|
||||||
|
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
|
// Don't render UI until after mount to prevent hydration mismatch
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return null
|
return null
|
||||||
@ -54,12 +63,30 @@ export function SuccessStep() {
|
|||||||
|
|
||||||
{/* Success header */}
|
{/* Success header */}
|
||||||
<h2 className={`text-2xl font-medium ${isDarkMode ? "text-white" : "text-zinc-900"} text-center mb-2`}>
|
<h2 className={`text-2xl font-medium ${isDarkMode ? "text-white" : "text-zinc-900"} text-center mb-2`}>
|
||||||
Successfully
|
Successfully Deployed!
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-center text-zinc-500 mb-8">
|
<p className="text-center text-zinc-500 mb-8">
|
||||||
Your auction was successfully created
|
Your project has been deployed successfully
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{/* Deployment summary */}
|
||||||
|
<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>
|
||||||
|
<span className="font-medium">{repoName}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">URL:</span>
|
||||||
|
<span className="font-mono text-xs">{deploymentUrl}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">Project ID:</span>
|
||||||
|
<span className="font-mono text-xs">{projectId}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Next steps section */}
|
{/* Next steps section */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h3 className={`text-lg font-medium ${isDarkMode ? "text-white" : "text-zinc-900"} mb-4`}>Next steps</h3>
|
<h3 className={`text-lg font-medium ${isDarkMode ? "text-white" : "text-zinc-900"} mb-4`}>Next steps</h3>
|
||||||
@ -84,12 +111,20 @@ export function SuccessStep() {
|
|||||||
<Button
|
<Button
|
||||||
className="w-full bg-white hover:bg-white/90 text-black flex items-center justify-center"
|
className="w-full bg-white hover:bg-white/90 text-black flex items-center justify-center"
|
||||||
onClick={handleViewProject}
|
onClick={handleViewProject}
|
||||||
|
disabled={!projectId || projectId === 'unknown-id'}
|
||||||
>
|
>
|
||||||
View Project
|
View Project
|
||||||
<svg className="ml-2 h-4 w-4" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg 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"/>
|
<path d="M5 12H19M19 12L13 6M19 12L13 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{/* Manual navigation button if auto-redirect fails */}
|
||||||
|
{projectId === 'unknown-id' && (
|
||||||
|
<p className="text-center text-xs text-muted-foreground">
|
||||||
|
Project ID not found. Please navigate to projects manually.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
// FixedProjectCard.tsx - With original components
|
// FixedProjectCard.tsx - With fixed navigation
|
||||||
|
'use client';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import React, { type ComponentPropsWithoutRef, useCallback } from 'react'
|
import React, { type ComponentPropsWithoutRef, useCallback } from 'react'
|
||||||
import { Card, CardContent, CardHeader } from '@workspace/ui/components/card'
|
import { Card, CardContent, CardHeader } from '@workspace/ui/components/card'
|
||||||
@ -9,7 +11,6 @@ import {
|
|||||||
AvatarFallback,
|
AvatarFallback,
|
||||||
AvatarImage
|
AvatarImage
|
||||||
} from '@workspace/ui/components/avatar'
|
} from '@workspace/ui/components/avatar'
|
||||||
import router from 'next/router'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Status types for project deployment status
|
* Status types for project deployment status
|
||||||
@ -92,6 +93,15 @@ const ProjectCardActions = ({
|
|||||||
setMenuOpen(!menuOpen);
|
setMenuOpen(!menuOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Close menu when clicking outside
|
||||||
|
React.useEffect(() => {
|
||||||
|
const handleClickOutside = () => setMenuOpen(false);
|
||||||
|
if (menuOpen) {
|
||||||
|
document.addEventListener('click', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('click', handleClickOutside);
|
||||||
|
}
|
||||||
|
}, [menuOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
@ -123,12 +133,6 @@ const ProjectCardActions = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function startDeployment(project: { project: any }) {
|
|
||||||
// Store selected project in state or localStorage
|
|
||||||
// Navigate to configuration page
|
|
||||||
router.push(`/projects/github/ps/create/cr/configure?repo=${project.id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FixedProjectCard component
|
* FixedProjectCard component
|
||||||
*/
|
*/
|
||||||
@ -155,19 +159,21 @@ export const FixedProjectCard = ({
|
|||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
router.push(`/projects/github/ps/${project.id}/settings`);
|
// Navigate to the settings tab - using the correct path structure
|
||||||
|
router.push(`/projects/github/ps/${project.id}/set`);
|
||||||
},
|
},
|
||||||
[project.id, router]
|
[project.id, router]
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles click on the delete menu item
|
* Handles click on the delete menu item - navigates to settings with delete intent
|
||||||
*/
|
*/
|
||||||
const handleDeleteClick = useCallback(
|
const handleDeleteClick = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
router.push(`/projects/github/ps/${project.id}/settings`);
|
// Navigate to settings and add a query parameter to trigger delete modal
|
||||||
|
router.push(`/projects/github/ps/${project.id}/set?action=delete`);
|
||||||
},
|
},
|
||||||
[project.id, router]
|
[project.id, router]
|
||||||
);
|
);
|
||||||
@ -215,15 +221,6 @@ export const FixedProjectCard = ({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ProjectStatusDot status={status} />
|
<ProjectStatusDot status={status} />
|
||||||
<ProjectDeploymentInfo project={project} />
|
<ProjectDeploymentInfo project={project} />
|
||||||
{/* <div className="mt-4 flex justify-end">
|
|
||||||
<Button
|
|
||||||
onClick={() => startDeployment(project)}
|
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
Deploy
|
|
||||||
</Button>
|
|
||||||
</div> */}
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
|
// src/components/providers.tsx
|
||||||
'use client'
|
'use client'
|
||||||
import { ThemeProvider } from 'next-themes'
|
import { ThemeProvider } from 'next-themes'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import '@workspace/ui/globals.css'
|
import '@workspace/ui/globals.css'
|
||||||
import { Toaster } from 'sonner'
|
import { Toaster } from 'sonner'
|
||||||
import { OctokitProviderWithRouter } from '@/context/OctokitProviderWithRouter'
|
import { OctokitProviderWithRouter } from '@/context/OctokitProviderWithRouter'
|
||||||
import { WalletProvider } from '@/context/WalletContext'
|
import { WalletContextProvider } from '@/context/WalletContextProvider'
|
||||||
|
import { BackendProvider } from '@/context/BackendContext'
|
||||||
import { GQLClientProvider } from '@/context'
|
import { GQLClientProvider } from '@/context'
|
||||||
import { GQLClient } from '@workspace/gql-client'
|
import { GQLClient } from '@workspace/gql-client'
|
||||||
|
|
||||||
@ -16,10 +18,8 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|||||||
// Initialize GQLClient
|
// Initialize GQLClient
|
||||||
const initGQLClient = async () => {
|
const initGQLClient = async () => {
|
||||||
try {
|
try {
|
||||||
// Create a new instance of GQLClient
|
|
||||||
const client = new GQLClient({
|
const client = new GQLClient({
|
||||||
endpoint: process.env.NEXT_PUBLIC_GQL_ENDPOINT || 'https://api.snowballtools-base.example',
|
gqlEndpoint: 'http://localhost:8000/graphql',
|
||||||
// Add any auth headers or other configuration needed
|
|
||||||
})
|
})
|
||||||
setGqlClient(client)
|
setGqlClient(client)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -32,8 +32,7 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|||||||
initGQLClient()
|
initGQLClient()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading || !gqlClient) {
|
||||||
// You might want to add a loading indicator here
|
|
||||||
return <div>Loading...</div>
|
return <div>Loading...</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,18 +44,16 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
enableColorScheme
|
enableColorScheme
|
||||||
>
|
>
|
||||||
<>
|
<Toaster />
|
||||||
<Toaster />
|
<WalletContextProvider>
|
||||||
<WalletProvider>
|
<BackendProvider>
|
||||||
|
<GQLClientProvider client={gqlClient}>
|
||||||
<OctokitProviderWithRouter>
|
<OctokitProviderWithRouter>
|
||||||
{gqlClient && (
|
{children}
|
||||||
<GQLClientProvider client={gqlClient}>
|
|
||||||
{children}
|
|
||||||
</GQLClientProvider>
|
|
||||||
)}
|
|
||||||
</OctokitProviderWithRouter>
|
</OctokitProviderWithRouter>
|
||||||
</WalletProvider>
|
</GQLClientProvider>
|
||||||
</>
|
</BackendProvider>
|
||||||
|
</WalletContextProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
114
apps/deploy-fe/src/components/templates/TemplateCard.tsx
Normal file
114
apps/deploy-fe/src/components/templates/TemplateCard.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
// src/components/templates/TemplateCard.tsx
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { ArrowRight, Clock, CheckCircle2 } from 'lucide-react'
|
||||||
|
import { Button } from '@workspace/ui/components/button'
|
||||||
|
import { Badge } from '@workspace/ui/components/badge'
|
||||||
|
import { cn } from '@workspace/ui/lib/utils'
|
||||||
|
import { TemplateIcon, type TemplateIconType } from './TemplateIcon'
|
||||||
|
import type { TemplateDetail } from '@/constants/templates'
|
||||||
|
|
||||||
|
interface TemplateCardProps {
|
||||||
|
template: TemplateDetail
|
||||||
|
isSelected?: boolean
|
||||||
|
onSelect?: (template: TemplateDetail) => void
|
||||||
|
disabled?: boolean
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TemplateCard({
|
||||||
|
template,
|
||||||
|
isSelected = false,
|
||||||
|
onSelect,
|
||||||
|
disabled = false,
|
||||||
|
className
|
||||||
|
}: TemplateCardProps) {
|
||||||
|
const handleClick = () => {
|
||||||
|
if (disabled || template.isComingSoon) return
|
||||||
|
onSelect?.(template)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'group relative flex items-start gap-4 p-4 rounded-lg border transition-all cursor-pointer',
|
||||||
|
'hover:border-primary/50 hover:bg-accent/50',
|
||||||
|
isSelected && 'border-primary bg-primary/5',
|
||||||
|
(disabled || template.isComingSoon) && 'opacity-50 cursor-not-allowed',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
{/* Icon */}
|
||||||
|
<div className={cn(
|
||||||
|
'flex items-center justify-center w-12 h-12 rounded-lg border',
|
||||||
|
'bg-background shadow-sm',
|
||||||
|
isSelected && 'border-primary bg-primary/10'
|
||||||
|
)}>
|
||||||
|
<TemplateIcon
|
||||||
|
type={template.icon as TemplateIconType}
|
||||||
|
size={24}
|
||||||
|
className={isSelected ? 'text-primary' : 'text-muted-foreground'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<h3 className="font-medium text-sm leading-tight">
|
||||||
|
{template.name}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Selection indicator */}
|
||||||
|
{isSelected && (
|
||||||
|
<CheckCircle2 className="h-5 w-5 text-primary flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-muted-foreground leading-relaxed mb-3">
|
||||||
|
{template.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Tags */}
|
||||||
|
{template.tags && template.tags.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1 mb-3">
|
||||||
|
{template.tags.map((tag) => (
|
||||||
|
<Badge
|
||||||
|
key={tag}
|
||||||
|
variant="secondary"
|
||||||
|
className="text-xs px-2 py-0.5"
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span className="text-muted-foreground font-mono">
|
||||||
|
{template.repoFullName}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{template.isComingSoon ? (
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
<Clock className="w-3 h-3 mr-1" />
|
||||||
|
Coming Soon
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className={cn(
|
||||||
|
'h-auto p-1 opacity-0 group-hover:opacity-100 transition-opacity',
|
||||||
|
isSelected && 'opacity-100'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ArrowRight className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
37
apps/deploy-fe/src/components/templates/TemplateIcon.tsx
Normal file
37
apps/deploy-fe/src/components/templates/TemplateIcon.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// src/components/templates/TemplateIcon.tsx
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Globe,
|
||||||
|
Image,
|
||||||
|
Code,
|
||||||
|
Smartphone,
|
||||||
|
Layout} from 'lucide-react'
|
||||||
|
|
||||||
|
export type TemplateIconType = 'web' | 'image' | 'nextjs' | 'pwa' | 'code' | 'mobile' | 'layout'
|
||||||
|
|
||||||
|
interface TemplateIconProps {
|
||||||
|
type: TemplateIconType
|
||||||
|
size?: number
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconMap = {
|
||||||
|
web: Globe,
|
||||||
|
image: Image,
|
||||||
|
nextjs: Code,
|
||||||
|
pwa: Smartphone,
|
||||||
|
code: Code,
|
||||||
|
mobile: Smartphone,
|
||||||
|
layout: Layout,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TemplateIcon({ type, size = 24, className = '' }: TemplateIconProps) {
|
||||||
|
const IconComponent = iconMap[type] || Code
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex items-center justify-center ${className}`}>
|
||||||
|
<IconComponent size={size} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
109
apps/deploy-fe/src/components/templates/TemplateSelection.tsx
Normal file
109
apps/deploy-fe/src/components/templates/TemplateSelection.tsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
// src/components/templates/TemplateSelection.tsx
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card'
|
||||||
|
import { Alert, AlertDescription } from '@workspace/ui/components/alert'
|
||||||
|
import { Badge } from '@workspace/ui/components/badge'
|
||||||
|
import { Info, Sparkles } from 'lucide-react'
|
||||||
|
import { TemplateCard } from './TemplateCard'
|
||||||
|
import { AVAILABLE_TEMPLATES, type TemplateDetail } from '@/constants/templates'
|
||||||
|
|
||||||
|
interface TemplateSelectionProps {
|
||||||
|
selectedTemplate?: TemplateDetail
|
||||||
|
onTemplateSelect: (template: TemplateDetail) => void
|
||||||
|
disabled?: boolean
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TemplateSelection({
|
||||||
|
selectedTemplate,
|
||||||
|
onTemplateSelect,
|
||||||
|
disabled = false,
|
||||||
|
className
|
||||||
|
}: TemplateSelectionProps) {
|
||||||
|
const [hoveredTemplate, setHoveredTemplate] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const availableTemplates = AVAILABLE_TEMPLATES.filter(t => !t.isComingSoon)
|
||||||
|
const comingSoonTemplates = AVAILABLE_TEMPLATES.filter(t => t.isComingSoon)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className={className}>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-base">
|
||||||
|
<Sparkles className="h-5 w-5 text-primary" />
|
||||||
|
Choose a Template
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<Alert>
|
||||||
|
<Info className="h-4 w-4" />
|
||||||
|
<AlertDescription className="text-sm">
|
||||||
|
Templates create a new repository in your GitHub account with pre-configured code.
|
||||||
|
You can customize it after deployment.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
{/* Available Templates */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-medium">Available Templates</h3>
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{availableTemplates.length} templates
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-3">
|
||||||
|
{availableTemplates.map((template) => (
|
||||||
|
<TemplateCard
|
||||||
|
key={template.id}
|
||||||
|
template={template}
|
||||||
|
isSelected={selectedTemplate?.id === template.id}
|
||||||
|
onSelect={onTemplateSelect}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Coming Soon Templates */}
|
||||||
|
{comingSoonTemplates.length > 0 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-medium text-muted-foreground">Coming Soon</h3>
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{comingSoonTemplates.length} more
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-3">
|
||||||
|
{comingSoonTemplates.map((template) => (
|
||||||
|
<TemplateCard
|
||||||
|
key={template.id}
|
||||||
|
template={template}
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Selection Summary */}
|
||||||
|
{selectedTemplate && (
|
||||||
|
<div className="p-3 bg-primary/5 border border-primary/20 rounded-md">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<div className="w-2 h-2 bg-primary rounded-full"></div>
|
||||||
|
<span className="text-sm font-medium">Selected Template</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
<strong>{selectedTemplate.name}</strong> will be forked to your GitHub account
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1 font-mono">
|
||||||
|
Source: {selectedTemplate.repoFullName}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
46
apps/deploy-fe/src/constants/templates.tsx
Normal file
46
apps/deploy-fe/src/constants/templates.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
// src/constants/templates.ts
|
||||||
|
export const TEMPLATE_REPOS = {
|
||||||
|
PWA: process.env.NEXT_PUBLIC_GITHUB_PWA_TEMPLATE_REPO || 'snowball-test/test-progressive-web-app',
|
||||||
|
IMAGE_UPLOAD_PWA: process.env.NEXT_PUBLIC_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO || 'snowball-test/image-upload-pwa-example',
|
||||||
|
NEXTJS: process.env.NEXT_PUBLIC_GITHUB_NEXT_APP_TEMPLATE_REPO || 'snowball-test/starter.nextjs-react-tailwind',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TemplateDetail {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
icon: string
|
||||||
|
repoFullName: string
|
||||||
|
description: string
|
||||||
|
isComingSoon?: boolean
|
||||||
|
tags?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AVAILABLE_TEMPLATES: TemplateDetail[] = [
|
||||||
|
{
|
||||||
|
id: 'pwa',
|
||||||
|
name: 'Progressive Web App (PWA)',
|
||||||
|
icon: 'web',
|
||||||
|
repoFullName: TEMPLATE_REPOS.PWA,
|
||||||
|
description: 'A fast, offline-capable web application with service worker support',
|
||||||
|
tags: ['PWA', 'Service Worker', 'Offline'],
|
||||||
|
isComingSoon: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'image-upload-pwa',
|
||||||
|
name: 'Image Upload PWA',
|
||||||
|
icon: 'image',
|
||||||
|
repoFullName: TEMPLATE_REPOS.IMAGE_UPLOAD_PWA,
|
||||||
|
description: 'PWA with image upload and processing capabilities',
|
||||||
|
tags: ['PWA', 'Upload', 'Images'],
|
||||||
|
isComingSoon: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'nextjs-tailwind',
|
||||||
|
name: 'Next.js + React + TailwindCSS',
|
||||||
|
icon: 'nextjs',
|
||||||
|
repoFullName: TEMPLATE_REPOS.NEXTJS,
|
||||||
|
description: 'Modern React framework with TailwindCSS for styling',
|
||||||
|
tags: ['Next.js', 'React', 'TailwindCSS'],
|
||||||
|
isComingSoon: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
111
apps/deploy-fe/src/context/BackendContext.tsx
Normal file
111
apps/deploy-fe/src/context/BackendContext.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
// src/context/BackendContext.tsx
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import type React from 'react'
|
||||||
|
import {
|
||||||
|
type ReactNode,
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
useCallback
|
||||||
|
} from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @interface BackendContextType
|
||||||
|
* @description Defines the structure of the BackendContext value.
|
||||||
|
*/
|
||||||
|
interface BackendContextType {
|
||||||
|
// Connection status
|
||||||
|
isBackendConnected: boolean
|
||||||
|
isLoading: boolean
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
checkBackendConnection: () => Promise<void>
|
||||||
|
refreshStatus: () => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @const BackendContext
|
||||||
|
* @description Creates a context for managing backend connection.
|
||||||
|
*/
|
||||||
|
const BackendContext = createContext<BackendContextType | undefined>(undefined)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @component BackendProvider
|
||||||
|
* @description Provides the BackendContext to its children.
|
||||||
|
*/
|
||||||
|
export const BackendProvider: React.FC<{ children: ReactNode }> = ({
|
||||||
|
children
|
||||||
|
}) => {
|
||||||
|
// State
|
||||||
|
const [isBackendConnected, setIsBackendConnected] = useState(false)
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
|
||||||
|
// Check backend connection
|
||||||
|
const checkBackendConnection = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://localhost:8000/auth/session', {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
|
||||||
|
const connected = response.ok
|
||||||
|
setIsBackendConnected(connected)
|
||||||
|
|
||||||
|
if (connected) {
|
||||||
|
console.log('✅ Backend connected')
|
||||||
|
} else {
|
||||||
|
console.log('❌ Backend not connected')
|
||||||
|
}
|
||||||
|
|
||||||
|
return connected
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking backend connection:', error)
|
||||||
|
setIsBackendConnected(false)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Refresh backend status
|
||||||
|
const refreshStatus = useCallback(async () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
try {
|
||||||
|
await checkBackendConnection()
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}, [checkBackendConnection])
|
||||||
|
|
||||||
|
// Initialize on mount
|
||||||
|
useEffect(() => {
|
||||||
|
refreshStatus()
|
||||||
|
}, [refreshStatus])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BackendContext.Provider
|
||||||
|
value={{
|
||||||
|
isBackendConnected,
|
||||||
|
isLoading,
|
||||||
|
checkBackendConnection,
|
||||||
|
refreshStatus
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</BackendContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function useBackend
|
||||||
|
* @description A hook that provides access to the BackendContext.
|
||||||
|
* @returns {BackendContextType} The backend context value.
|
||||||
|
* @throws {Error} If used outside of a BackendProvider.
|
||||||
|
*/
|
||||||
|
export const useBackend = () => {
|
||||||
|
const context = useContext(BackendContext)
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useBackend must be used within a BackendProvider')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
// src/context/OctokitContext.tsx
|
||||||
import { Octokit, RequestError } from 'octokit'
|
import { Octokit, RequestError } from 'octokit'
|
||||||
|
|
||||||
import { useDebounceCallback } from 'usehooks-ts'
|
import { useDebounceCallback } from 'usehooks-ts'
|
||||||
@ -55,21 +56,40 @@ export const OctokitProvider = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [authToken, setAuthToken] = useState<string | null>(null)
|
const [authToken, setAuthToken] = useState<string | null>(null)
|
||||||
const [isAuth, setIsAuth] = useState(false)
|
const [isAuth, setIsAuth] = useState(false)
|
||||||
// const navigate = externalNavigate || internalNavigateconst
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { orgSlug } = useParams()
|
const { orgSlug } = useParams()
|
||||||
// const { toast, dismiss } = useToast()
|
const gqlClient = useGQLClient()
|
||||||
const client = useGQLClient()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @function fetchUser
|
* @function fetchUser
|
||||||
* @description Fetches the user's GitHub token from the GQLClient.
|
* @description Fetches the user's GitHub token from the GQLClient.
|
||||||
*/
|
*/
|
||||||
const fetchUser = useCallback(async () => {
|
const fetchUser = useCallback(async () => {
|
||||||
const { user } = await client.getUser()
|
try {
|
||||||
|
console.log('🔍 Fetching user data from GraphQL...')
|
||||||
|
|
||||||
|
// Check if gqlClient exists and has the getUser method
|
||||||
|
if (!gqlClient || typeof gqlClient.getUser !== 'function') {
|
||||||
|
console.error('❌ GQL client not available or getUser method missing')
|
||||||
|
setAuthToken(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setAuthToken(user.gitHubToken)
|
const { user } = await gqlClient.getUser()
|
||||||
}, [client])
|
console.log('📊 User data received:', user)
|
||||||
|
|
||||||
|
if (user && user.gitHubToken) {
|
||||||
|
console.log('✅ GitHub token found in user data')
|
||||||
|
setAuthToken(user.gitHubToken)
|
||||||
|
} else {
|
||||||
|
console.log('❌ No GitHub token found in user data')
|
||||||
|
setAuthToken(null)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error fetching user:', error)
|
||||||
|
setAuthToken(null)
|
||||||
|
}
|
||||||
|
}, [gqlClient])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @function updateAuth
|
* @function updateAuth
|
||||||
@ -82,25 +102,29 @@ export const OctokitProvider = ({
|
|||||||
const octokit = useMemo(() => {
|
const octokit = useMemo(() => {
|
||||||
if (!authToken) {
|
if (!authToken) {
|
||||||
setIsAuth(false)
|
setIsAuth(false)
|
||||||
|
console.log('🐙 Creating Octokit without auth token')
|
||||||
return new Octokit()
|
return new Octokit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('🐙 Creating Octokit with auth token')
|
||||||
setIsAuth(true)
|
setIsAuth(true)
|
||||||
return new Octokit({ auth: authToken })
|
return new Octokit({ auth: authToken })
|
||||||
}, [authToken])
|
}, [authToken])
|
||||||
|
|
||||||
|
// Only fetch user when GQL client is available
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUser()
|
if (gqlClient) {
|
||||||
}, [fetchUser])
|
console.log('🔄 GQL client available, fetching user...')
|
||||||
|
fetchUser()
|
||||||
|
} else {
|
||||||
|
console.log('⏳ Waiting for GQL client...')
|
||||||
|
}
|
||||||
|
}, [gqlClient, fetchUser])
|
||||||
|
|
||||||
const debouncedUnauthorizedGithubHandler = useDebounceCallback(
|
const debouncedUnauthorizedGithubHandler = useDebounceCallback(
|
||||||
useCallback(
|
useCallback(
|
||||||
(error: RequestError) => {
|
(error: RequestError) => {
|
||||||
toast.error(`GitHub authentication error: ${error.message}`, {
|
toast.error(`GitHub authentication error: ${error.message}`)
|
||||||
// id: 'unauthorized-github-token',
|
|
||||||
// variant: 'error',
|
|
||||||
// onDismiss: dismiss
|
|
||||||
})
|
|
||||||
|
|
||||||
router.push(`/${orgSlug}/projects/create`)
|
router.push(`/${orgSlug}/projects/create`)
|
||||||
},
|
},
|
||||||
@ -116,7 +140,9 @@ export const OctokitProvider = ({
|
|||||||
error instanceof RequestError &&
|
error instanceof RequestError &&
|
||||||
error.status === UNAUTHORIZED_ERROR_CODE
|
error.status === UNAUTHORIZED_ERROR_CODE
|
||||||
) {
|
) {
|
||||||
await client.unauthenticateGithub()
|
if (gqlClient && typeof gqlClient.unauthenticateGithub === 'function') {
|
||||||
|
await gqlClient.unauthenticateGithub()
|
||||||
|
}
|
||||||
await fetchUser()
|
await fetchUser()
|
||||||
|
|
||||||
debouncedUnauthorizedGithubHandler(error)
|
debouncedUnauthorizedGithubHandler(error)
|
||||||
@ -131,7 +157,7 @@ export const OctokitProvider = ({
|
|||||||
// Remove the interceptor when the component unmounts
|
// Remove the interceptor when the component unmounts
|
||||||
octokit.hook.remove('request', interceptor)
|
octokit.hook.remove('request', interceptor)
|
||||||
}
|
}
|
||||||
}, [octokit, client, debouncedUnauthorizedGithubHandler, fetchUser])
|
}, [octokit, gqlClient, debouncedUnauthorizedGithubHandler, fetchUser])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OctokitContext.Provider value={{ octokit, updateAuth, isAuth }}>
|
<OctokitContext.Provider value={{ octokit, updateAuth, isAuth }}>
|
||||||
@ -153,4 +179,4 @@ export const useOctokit = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
@ -1,183 +1,46 @@
|
|||||||
import type React from 'react'
|
// src/context/WalletContext.tsx
|
||||||
import {
|
'use client'
|
||||||
type ReactNode,
|
|
||||||
createContext,
|
import { createContext, useContext } from 'react'
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useState
|
|
||||||
} from 'react'
|
|
||||||
import { toast } from 'sonner'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @interface WalletContextType
|
* Wallet Context Interface
|
||||||
* @description Defines the structure of the WalletContext value.
|
|
||||||
* @property {object | null} wallet - The wallet object containing id and address.
|
|
||||||
* @property {boolean} isConnected - Indicates if the wallet is connected.
|
|
||||||
* @property {function} connect - Function to connect the wallet.
|
|
||||||
* @property {function} disconnect - Function to disconnect the wallet.
|
|
||||||
*/
|
*/
|
||||||
interface WalletContextType {
|
export interface WalletContextType {
|
||||||
|
// Wallet state
|
||||||
wallet: {
|
wallet: {
|
||||||
id: string
|
id: string
|
||||||
address?: string
|
address: string
|
||||||
} | null
|
} | null
|
||||||
isConnected: boolean
|
|
||||||
|
// Connection states
|
||||||
|
isConnected: boolean // SIWE authenticated + backend session
|
||||||
|
hasWalletAddress: boolean // Just has wallet address
|
||||||
|
isLoading: boolean // Connection/auth in progress
|
||||||
|
|
||||||
|
// Actions
|
||||||
connect: () => Promise<void>
|
connect: () => Promise<void>
|
||||||
disconnect: () => void
|
disconnect: () => void
|
||||||
|
checkSession: () => Promise<boolean>
|
||||||
|
|
||||||
|
// Debug info
|
||||||
|
lastError?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @const WalletContext
|
* Wallet Context
|
||||||
* @description Creates a context for managing wallet connection state.
|
|
||||||
*/
|
*/
|
||||||
const WalletContext = createContext<WalletContextType | undefined>(undefined)
|
export const WalletContext = createContext<WalletContextType | undefined>(undefined)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @component WalletProvider
|
* useWallet Hook
|
||||||
* @description Provides the WalletContext to its children.
|
* @description Hook to access wallet context. Must be used within WalletProvider.
|
||||||
* @param {Object} props - Component props
|
* @throws Error if used outside WalletProvider
|
||||||
* @param {ReactNode} props.children - The children to render.
|
|
||||||
*/
|
*/
|
||||||
export const WalletProvider: React.FC<{ children: ReactNode }> = ({
|
export const useWallet = (): WalletContextType => {
|
||||||
children
|
|
||||||
}) => {
|
|
||||||
const [wallet, setWallet] = useState<WalletContextType['wallet']>(null)
|
|
||||||
const [isConnected, setIsConnected] = useState(false)
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// const handleWalletMessage = (event: MessageEvent) => {
|
|
||||||
// if (event.origin !== process.env.NEXT_PUBLIC_WALLET_IFRAME_URL) return
|
|
||||||
|
|
||||||
// if (event.data.type === 'WALLET_ACCOUNTS_DATA') {
|
|
||||||
// const address = event.data.data[0].address
|
|
||||||
// setWallet({
|
|
||||||
// id: address,
|
|
||||||
// address: address
|
|
||||||
// })
|
|
||||||
// setIsConnected(true)
|
|
||||||
// toast.success('Wallet Connected', {
|
|
||||||
// // variant: 'success',
|
|
||||||
// duration: 3000
|
|
||||||
// // id: '',
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// window.addEventListener('message', handleWalletMessage)
|
|
||||||
// return () => window.removeEventListener('message', handleWalletMessage)
|
|
||||||
// }, [])
|
|
||||||
|
|
||||||
// const connect = async () => {
|
|
||||||
// const iframe = document.getElementById('checkBalanceIframe') as HTMLIFrameElement
|
|
||||||
// // const iframe = document.getElementById('walletIframe') as HTMLIFrameElement
|
|
||||||
// if (iframe?.contentWindow) {
|
|
||||||
// iframe.contentWindow.postMessage(
|
|
||||||
// {
|
|
||||||
// type: 'REQUEST_WALLET_ACCOUNTS',
|
|
||||||
// chainId: process.env.NEXT_PUBLIC_LACONICD_CHAIN_ID
|
|
||||||
// },
|
|
||||||
// process.env.NEXT_PUBLIC_WALLET_IFRAME_URL ?? ''
|
|
||||||
// )
|
|
||||||
// } else {
|
|
||||||
// toast.error('Wallet Connection Failed', {
|
|
||||||
// // description: 'Wallet iframe not found or not loaded',
|
|
||||||
// // variant: 'error',
|
|
||||||
// duration: 3000
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
const connect = async () => {
|
|
||||||
console.log("🔌 Attempting to connect wallet...")
|
|
||||||
console.log("🔧 Environment variables:", {
|
|
||||||
NEXT_PUBLIC_WALLET_IFRAME_URL: process.env.NEXT_PUBLIC_WALLET_IFRAME_URL,
|
|
||||||
NEXT_PUBLIC_LACONICD_CHAIN_ID: process.env.NEXT_PUBLIC_LACONICD_CHAIN_ID
|
|
||||||
})
|
|
||||||
|
|
||||||
const iframe = document.getElementById('checkBalanceIframe') as HTMLIFrameElement
|
|
||||||
console.log("📱 Iframe element:", iframe)
|
|
||||||
|
|
||||||
if (iframe?.contentWindow) {
|
|
||||||
console.log("💬 Sending message to iframe:", {
|
|
||||||
type: 'REQUEST_WALLET_ACCOUNTS',
|
|
||||||
chainId: process.env.NEXT_PUBLIC_LACONICD_CHAIN_ID
|
|
||||||
})
|
|
||||||
|
|
||||||
iframe.contentWindow.postMessage(
|
|
||||||
{
|
|
||||||
type: 'REQUEST_WALLET_ACCOUNTS',
|
|
||||||
chainId: process.env.NEXT_PUBLIC_LACONICD_CHAIN_ID
|
|
||||||
},
|
|
||||||
process.env.NEXT_PUBLIC_WALLET_IFRAME_URL ?? ''
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
console.error("❌ Iframe not found or not loaded:", {
|
|
||||||
iframe: iframe,
|
|
||||||
contentWindow: iframe?.contentWindow
|
|
||||||
})
|
|
||||||
toast.error('Wallet Connection Failed', {
|
|
||||||
duration: 3000
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add logging to the message handler
|
|
||||||
useEffect(() => {
|
|
||||||
const handleWalletMessage = (event: MessageEvent) => {
|
|
||||||
console.log("📨 Received message from wallet:", event.data)
|
|
||||||
|
|
||||||
if (event.origin !== process.env.NEXT_PUBLIC_WALLET_IFRAME_URL) {
|
|
||||||
console.warn("⚠️ Message from unexpected origin:", event.origin)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.data.type === 'WALLET_ACCOUNTS_DATA') {
|
|
||||||
console.log("✅ Wallet accounts data received:", event.data)
|
|
||||||
const address = event.data.data[0].address
|
|
||||||
setWallet({
|
|
||||||
id: address,
|
|
||||||
address: address
|
|
||||||
})
|
|
||||||
setIsConnected(true)
|
|
||||||
toast.success('Wallet Connected', {
|
|
||||||
duration: 3000
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
console.log("📤 Received message type:", event.data.type, event.data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('message', handleWalletMessage)
|
|
||||||
return () => window.removeEventListener('message', handleWalletMessage)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const disconnect = () => {
|
|
||||||
setWallet(null)
|
|
||||||
setIsConnected(false)
|
|
||||||
toast.info('Wallet Disconnected', {
|
|
||||||
duration: 3000
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<WalletContext.Provider
|
|
||||||
value={{ wallet, isConnected, connect, disconnect }}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</WalletContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @function useWallet
|
|
||||||
* @description A hook that provides access to the WalletContext.
|
|
||||||
* @returns {WalletContextType} The wallet context value.
|
|
||||||
* @throws {Error} If used outside of a WalletProvider.
|
|
||||||
*/
|
|
||||||
export const useWallet = () => {
|
|
||||||
const context = useContext(WalletContext)
|
const context = useContext(WalletContext)
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
||||||
throw new Error('useWallet must be used within a WalletProvider')
|
throw new Error('useWallet must be used within a WalletProvider')
|
||||||
}
|
}
|
||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
@ -1,246 +1,255 @@
|
|||||||
import { AutoSignInIFrameModal } from '@/components/iframe/auto-sign-in'
|
// src/context/WalletProvider.tsx
|
||||||
import axios from 'axios'
|
'use client'
|
||||||
import { usePathname, useRouter } from 'next/navigation'
|
|
||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
import {
|
import { type ReactNode, useState, useEffect, useCallback } from 'react'
|
||||||
type ReactNode,
|
|
||||||
createContext,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useState
|
|
||||||
} from 'react'
|
|
||||||
import { SiweMessage, generateNonce } from 'siwe'
|
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
import { WalletContext, type WalletContextType } from './WalletContext'
|
||||||
|
import { AutoSignInIFrameModal } from '@/components/iframe/auto-sign-in'
|
||||||
|
|
||||||
const axiosInstance = axios.create({
|
// Environment variables
|
||||||
baseURL: process.env.NEXT_PUBLIC_API_URL,
|
const BACKEND_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'
|
||||||
headers: {
|
const WALLET_IFRAME_URL = process.env.NEXT_PUBLIC_WALLET_IFRAME_URL || 'http://localhost:4000'
|
||||||
'Content-Type': 'application/json',
|
const WALLET_IFRAME_ID = 'wallet-communication-iframe'
|
||||||
'Access-Control-Allow-Origin': '*'
|
|
||||||
},
|
|
||||||
withCredentials: true
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
export const WalletContextProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
* @interface WalletContextType
|
// Core wallet state
|
||||||
* @description Defines the structure of the WalletContext value.
|
|
||||||
* @property {object | null} wallet - The wallet object containing id and address.
|
|
||||||
* @property {boolean} isConnected - Indicates if the wallet is connected.
|
|
||||||
* @property {boolean} isReady - Indicates if the app is ready to make API calls.
|
|
||||||
* @property {function} connect - Function to connect the wallet.
|
|
||||||
* @property {function} disconnect - Function to disconnect the wallet.
|
|
||||||
*/
|
|
||||||
interface WalletContextType {
|
|
||||||
wallet: {
|
|
||||||
id: string
|
|
||||||
address?: string
|
|
||||||
} | null
|
|
||||||
isConnected: boolean
|
|
||||||
isReady: boolean
|
|
||||||
connect: () => Promise<void>
|
|
||||||
disconnect: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @const WalletContext
|
|
||||||
* @description Creates a context for managing wallet connection state.
|
|
||||||
*/
|
|
||||||
const WalletContext = createContext<WalletContextType | undefined>(undefined)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @component WalletContextProvider
|
|
||||||
* @description Provides the WalletContext to its children.
|
|
||||||
* @param {Object} props - Component props
|
|
||||||
* @param {ReactNode} props.children - The children to render.
|
|
||||||
*/
|
|
||||||
export const WalletContextProvider: React.FC<{ children: ReactNode }> = ({
|
|
||||||
children
|
|
||||||
}) => {
|
|
||||||
const [wallet, setWallet] = useState<WalletContextType['wallet']>(null)
|
const [wallet, setWallet] = useState<WalletContextType['wallet']>(null)
|
||||||
const [isConnected, setIsConnected] = useState(false)
|
const [isConnected, setIsConnected] = useState(false)
|
||||||
const [isReady, setIsReady] = useState(false)
|
const [hasWalletAddress, setHasWalletAddress] = useState(false)
|
||||||
const [accountAddress, setAccountAddress] = useState<string>()
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const router = useRouter()
|
const [lastError, setLastError] = useState<string>()
|
||||||
const pathname = usePathname()
|
|
||||||
const baseUrl = process.env.NEXT_PUBLIC_API_URL
|
// Modal state for SIWE authentication
|
||||||
|
const [showAuthModal, setShowAuthModal] = useState(false)
|
||||||
|
|
||||||
// Update isReady state when connection changes
|
// Check if we have an active backend session
|
||||||
useEffect(() => {
|
const checkSession = useCallback(async (): Promise<boolean> => {
|
||||||
if (isConnected) {
|
try {
|
||||||
// Add a small delay to ensure session is fully established
|
const response = await fetch(`${BACKEND_URL}/auth/session`, {
|
||||||
const timer = setTimeout(() => {
|
method: 'GET',
|
||||||
setIsReady(true)
|
credentials: 'include',
|
||||||
console.log('Wallet is now ready for API calls')
|
})
|
||||||
}, 500)
|
|
||||||
return () => clearTimeout(timer)
|
const sessionExists = response.ok
|
||||||
}
|
setIsConnected(sessionExists)
|
||||||
setIsReady(false)
|
|
||||||
}, [isConnected])
|
if (sessionExists) {
|
||||||
|
console.log('✅ Active wallet session found')
|
||||||
// Check session status on mount
|
|
||||||
useEffect(() => {
|
|
||||||
fetch(`${baseUrl}/auth/session`, {
|
|
||||||
credentials: 'include'
|
|
||||||
}).then((res) => {
|
|
||||||
const path = pathname
|
|
||||||
console.log(res)
|
|
||||||
if (res.status !== 200) {
|
|
||||||
setIsConnected(false)
|
|
||||||
localStorage.clear()
|
|
||||||
if (path !== '/login') {
|
|
||||||
router.push('/login')
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
setIsConnected(true)
|
console.log('❌ No active wallet session')
|
||||||
if (path === '/login') {
|
|
||||||
router.push('/')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}, [pathname, router, baseUrl])
|
return sessionExists
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking wallet session:', error)
|
||||||
|
setLastError('Failed to check session')
|
||||||
|
setIsConnected(false)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
// Handle wallet messages for account data
|
// Initialize session check on mount
|
||||||
|
useEffect(() => {
|
||||||
|
checkSession()
|
||||||
|
}, [checkSession])
|
||||||
|
|
||||||
|
// Handle wallet messages from iframe
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleWalletMessage = (event: MessageEvent) => {
|
const handleWalletMessage = (event: MessageEvent) => {
|
||||||
if (event.origin !== process.env.NEXT_PUBLIC_WALLET_IFRAME_URL) return
|
// Security check
|
||||||
console.log(event)
|
if (event.origin !== WALLET_IFRAME_URL) {
|
||||||
|
console.warn('⚠️ Message from unexpected origin:', event.origin)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📨 Wallet message received:', {
|
||||||
|
type: event.data.type,
|
||||||
|
origin: event.origin,
|
||||||
|
data: event.data.data
|
||||||
|
})
|
||||||
|
|
||||||
if (event.data.type === 'WALLET_ACCOUNTS_DATA') {
|
if (event.data.type === 'WALLET_ACCOUNTS_DATA') {
|
||||||
const address = event.data.data[0].address
|
handleWalletAccountsData(event.data.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleWalletAccountsData = (data: any) => {
|
||||||
|
let address: string | undefined
|
||||||
|
|
||||||
|
// Handle different data formats from wallet
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
if (typeof data[0] === 'string') {
|
||||||
|
address = data[0]
|
||||||
|
} else if (data[0]?.address) {
|
||||||
|
address = data[0].address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (address) {
|
||||||
|
console.log('✅ Wallet address received:', address)
|
||||||
|
|
||||||
setWallet({
|
setWallet({
|
||||||
id: address,
|
id: address,
|
||||||
address: address
|
address: address
|
||||||
})
|
})
|
||||||
setAccountAddress(address)
|
setHasWalletAddress(true)
|
||||||
setIsConnected(true)
|
setLastError(undefined)
|
||||||
toast.success('Wallet Connected', {
|
|
||||||
// variant: 'success',
|
// Check if we already have a session for this wallet
|
||||||
duration: 3000
|
checkSession().then(hasSession => {
|
||||||
// id: '',
|
if (!hasSession) {
|
||||||
|
// Need SIWE authentication
|
||||||
|
console.log('🔐 Starting SIWE authentication...')
|
||||||
|
setShowAuthModal(true)
|
||||||
|
} else {
|
||||||
|
toast.success('Wallet connected!')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
console.error('❌ Could not extract address from wallet data:', data)
|
||||||
|
setLastError('Invalid wallet data received')
|
||||||
|
toast.error('Invalid wallet data received')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('message', handleWalletMessage)
|
window.addEventListener('message', handleWalletMessage)
|
||||||
return () => window.removeEventListener('message', handleWalletMessage)
|
return () => window.removeEventListener('message', handleWalletMessage)
|
||||||
}, [])
|
}, [checkSession])
|
||||||
|
|
||||||
// Handle sign-in response from the wallet iframe
|
// Connect to wallet
|
||||||
useEffect(() => {
|
const connect = useCallback(async () => {
|
||||||
const handleSignInResponse = async (event: MessageEvent) => {
|
if (isLoading) {
|
||||||
if (event.origin !== process.env.NEXT_PUBLIC_WALLET_IFRAME_URL) return
|
console.log('⏸️ Connection already in progress')
|
||||||
|
return
|
||||||
if (event.data.type === 'SIGN_IN_RESPONSE') {
|
|
||||||
try {
|
|
||||||
const { success } = (
|
|
||||||
await axiosInstance.post('/auth/validate', {
|
|
||||||
message: event.data.data.message,
|
|
||||||
signature: event.data.data.signature
|
|
||||||
})
|
|
||||||
).data
|
|
||||||
|
|
||||||
if (success === true) {
|
|
||||||
setIsConnected(true)
|
|
||||||
if (pathname === '/login') {
|
|
||||||
router.push('/')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error signing in:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('message', handleSignInResponse)
|
setIsLoading(true)
|
||||||
|
setLastError(undefined)
|
||||||
return () => {
|
|
||||||
window.removeEventListener('message', handleSignInResponse)
|
try {
|
||||||
}
|
console.log('🔌 Attempting to connect wallet...')
|
||||||
}, [router, pathname])
|
|
||||||
|
// Find the wallet communication iframe
|
||||||
// Initiate auto sign-in when account address is available
|
const iframe = document.getElementById(WALLET_IFRAME_ID) as HTMLIFrameElement
|
||||||
useEffect(() => {
|
|
||||||
const initiateAutoSignIn = async () => {
|
if (!iframe) {
|
||||||
if (!accountAddress) return
|
throw new Error('Wallet communication interface not found')
|
||||||
|
|
||||||
const iframe = document.getElementById(
|
|
||||||
'walletAuthFrame'
|
|
||||||
) as HTMLIFrameElement
|
|
||||||
|
|
||||||
if (!iframe?.contentWindow) {
|
|
||||||
console.error('Iframe not found or not loaded')
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = new SiweMessage({
|
if (!iframe.contentWindow) {
|
||||||
version: '1',
|
throw new Error('Wallet interface not loaded')
|
||||||
domain: window.location.host,
|
}
|
||||||
uri: window.location.origin,
|
|
||||||
chainId: 1,
|
|
||||||
address: accountAddress,
|
|
||||||
nonce: generateNonce(),
|
|
||||||
statement: 'Sign in With Ethereum.'
|
|
||||||
}).prepareMessage()
|
|
||||||
|
|
||||||
iframe.contentWindow.postMessage(
|
console.log('📤 Sending wallet connection request...')
|
||||||
{
|
|
||||||
type: 'AUTO_SIGN_IN',
|
|
||||||
chainId: '1',
|
|
||||||
message
|
|
||||||
},
|
|
||||||
process.env.NEXT_PUBLIC_WALLET_IFRAME_URL ?? ''
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
initiateAutoSignIn()
|
|
||||||
}, [accountAddress])
|
|
||||||
|
|
||||||
const connect = async () => {
|
|
||||||
const iframe = document.getElementById('walletIframe') as HTMLIFrameElement
|
|
||||||
if (iframe?.contentWindow) {
|
|
||||||
iframe.contentWindow.postMessage(
|
iframe.contentWindow.postMessage(
|
||||||
{
|
{
|
||||||
type: 'REQUEST_WALLET_ACCOUNTS',
|
type: 'REQUEST_WALLET_ACCOUNTS',
|
||||||
chainId: process.env.NEXT_PUBLIC_LACONICD_CHAIN_ID
|
chainId: process.env.NEXT_PUBLIC_LACONICD_CHAIN_ID || '1'
|
||||||
},
|
},
|
||||||
process.env.NEXT_PUBLIC_WALLET_IFRAME_URL ?? ''
|
WALLET_IFRAME_URL
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
toast.error('Wallet Connection Failed', {
|
|
||||||
// description: 'Wallet iframe not found or not loaded',
|
|
||||||
// variant: 'error',
|
|
||||||
duration: 3000
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const disconnect = () => {
|
// Set a timeout for connection attempt
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
if (!hasWalletAddress) {
|
||||||
|
setLastError('Connection timeout')
|
||||||
|
toast.error('Wallet connection timeout. Please try again.')
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}, 15000) // 15 second timeout
|
||||||
|
|
||||||
|
// Clear timeout if we get an address
|
||||||
|
if (hasWalletAddress) {
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Failed to connect wallet'
|
||||||
|
console.error('❌ Error connecting wallet:', error)
|
||||||
|
setLastError(errorMessage)
|
||||||
|
toast.error(errorMessage)
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}, [isLoading, hasWalletAddress])
|
||||||
|
|
||||||
|
// Update loading state when address is received
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasWalletAddress && isLoading) {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}, [hasWalletAddress, isLoading])
|
||||||
|
|
||||||
|
// Handle successful SIWE authentication
|
||||||
|
const handleAuthComplete = useCallback((success: boolean) => {
|
||||||
|
if (success) {
|
||||||
|
setIsConnected(true)
|
||||||
|
toast.success('Wallet authentication complete!')
|
||||||
|
console.log('✅ SIWE authentication successful')
|
||||||
|
} else {
|
||||||
|
console.log('❌ SIWE authentication failed')
|
||||||
|
setLastError('SIWE authentication failed')
|
||||||
|
toast.error('Wallet authentication failed')
|
||||||
|
}
|
||||||
|
setShowAuthModal(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Disconnect wallet
|
||||||
|
const disconnect = useCallback(() => {
|
||||||
setWallet(null)
|
setWallet(null)
|
||||||
setIsConnected(false)
|
setIsConnected(false)
|
||||||
toast.info('Wallet Disconnected', {
|
setHasWalletAddress(false)
|
||||||
duration: 3000
|
setShowAuthModal(false)
|
||||||
})
|
setLastError(undefined)
|
||||||
|
|
||||||
|
// Call backend logout
|
||||||
|
fetch(`${BACKEND_URL}/auth/logout`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include'
|
||||||
|
}).catch(console.error)
|
||||||
|
|
||||||
|
toast.info('Wallet disconnected')
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const contextValue: WalletContextType = {
|
||||||
|
wallet,
|
||||||
|
isConnected,
|
||||||
|
hasWalletAddress,
|
||||||
|
isLoading,
|
||||||
|
connect,
|
||||||
|
disconnect,
|
||||||
|
checkSession,
|
||||||
|
lastError
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WalletContext.Provider
|
<WalletContext.Provider value={contextValue}>
|
||||||
value={{ wallet, isConnected, isReady, connect, disconnect }}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
{!isConnected && <AutoSignInIFrameModal />}
|
|
||||||
|
{/* Always-present hidden iframe for wallet communication */}
|
||||||
|
<iframe
|
||||||
|
id={WALLET_IFRAME_ID}
|
||||||
|
src={WALLET_IFRAME_URL}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: '-9999px',
|
||||||
|
width: '1px',
|
||||||
|
height: '1px',
|
||||||
|
opacity: 0,
|
||||||
|
pointerEvents: 'none'
|
||||||
|
}}
|
||||||
|
sandbox="allow-scripts allow-same-origin"
|
||||||
|
title="Wallet Communication Interface"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* SIWE Authentication Modal */}
|
||||||
|
{showAuthModal && (
|
||||||
|
<AutoSignInIFrameModal
|
||||||
|
onAuthComplete={handleAuthComplete}
|
||||||
|
onClose={() => setShowAuthModal(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</WalletContext.Provider>
|
</WalletContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @function useWallet
|
|
||||||
* @description A hook that provides access to the WalletContext.
|
|
||||||
* @returns {WalletContextType} The wallet context value.
|
|
||||||
* @throws {Error} If used outside of a WalletContextProvider.
|
|
||||||
*/
|
|
||||||
export const useWallet = () => {
|
|
||||||
const context = useContext(WalletContext)
|
|
||||||
if (context === undefined) {
|
|
||||||
throw new Error('useWallet must be used within a WalletContextProvider')
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
@ -1,169 +0,0 @@
|
|||||||
// "use client";
|
|
||||||
|
|
||||||
// import { useState, useEffect } from "react";
|
|
||||||
// import { Octokit } from "@octokit/rest";
|
|
||||||
|
|
||||||
// export function useRepoData(repoId: string) {
|
|
||||||
// const [repoData, setRepoData] = useState<any>(null);
|
|
||||||
// const [isLoading, setIsLoading] = useState(true);
|
|
||||||
// const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// async function fetchRepoData() {
|
|
||||||
// setIsLoading(true);
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// // Use the same hardcoded token as in projects/page.tsx
|
|
||||||
// const authToken = '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';
|
|
||||||
|
|
||||||
// // Create Octokit instance with token
|
|
||||||
// const octokit = new Octokit({
|
|
||||||
// auth: authToken
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // Fetch repos from GitHub
|
|
||||||
// const { data: repos } = await octokit.repos.listForAuthenticatedUser();
|
|
||||||
|
|
||||||
// // Find the specific repo by ID
|
|
||||||
// const repo = repos.find(repo => repo.id.toString() === repoId);
|
|
||||||
|
|
||||||
// if (!repo) {
|
|
||||||
// setError("Repository not found");
|
|
||||||
// setRepoData(null);
|
|
||||||
// } else {
|
|
||||||
// setRepoData(repo);
|
|
||||||
// setError(null);
|
|
||||||
// }
|
|
||||||
// } catch (err) {
|
|
||||||
// console.error('Error fetching GitHub repo:', err);
|
|
||||||
// setError('Failed to fetch repository data');
|
|
||||||
// setRepoData(null);
|
|
||||||
// } finally {
|
|
||||||
// setIsLoading(false);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fetchRepoData();
|
|
||||||
// }, [repoId]);
|
|
||||||
|
|
||||||
// return { repoData, isLoading, error };
|
|
||||||
// }
|
|
||||||
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import { useAuth, useUser } from "@clerk/nextjs";
|
|
||||||
import { Octokit } from "@octokit/rest";
|
|
||||||
|
|
||||||
// Define the return type of the hook
|
|
||||||
interface UseRepoDataReturn {
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Get auth data from Clerk
|
|
||||||
const { isLoaded: isAuthLoaded, userId } = useAuth();
|
|
||||||
const { isLoaded: isUserLoaded, user } = useUser();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let isMounted = true;
|
|
||||||
|
|
||||||
async function fetchRepoData() {
|
|
||||||
try {
|
|
||||||
if (!userId || !user) {
|
|
||||||
if (isMounted) {
|
|
||||||
setError("User not authenticated");
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the fallback token for now
|
|
||||||
// In production, this would be replaced with a more robust solution
|
|
||||||
const authToken = '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';
|
|
||||||
|
|
||||||
// Check for a different way to get GitHub authorization
|
|
||||||
let githubToken = authToken;
|
|
||||||
|
|
||||||
// Try to get from session storage if available (client-side only)
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
const storedToken = sessionStorage.getItem('github_token');
|
|
||||||
if (storedToken) {
|
|
||||||
console.log("Using token from session storage");
|
|
||||||
githubToken = storedToken;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create Octokit instance with token
|
|
||||||
const octokit = new Octokit({
|
|
||||||
auth: githubToken
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fetch repos from GitHub
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the specific repo by ID if repoId is provided
|
|
||||||
const repo = repos.find(repo => repo.id.toString() === repoId);
|
|
||||||
|
|
||||||
if (!repo) {
|
|
||||||
if (isMounted) {
|
|
||||||
setError("Repository not found");
|
|
||||||
setRepoData(null);
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (isMounted) {
|
|
||||||
setRepoData(repo);
|
|
||||||
setError(null);
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error fetching GitHub repo:', err);
|
|
||||||
if (isMounted) {
|
|
||||||
setError('Failed to fetch repository data');
|
|
||||||
setRepoData(null);
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only fetch if the user is authenticated and Clerk is loaded
|
|
||||||
if (isAuthLoaded && isUserLoaded && userId) {
|
|
||||||
fetchRepoData();
|
|
||||||
} else if (isAuthLoaded && isUserLoaded && !userId) {
|
|
||||||
if (isMounted) {
|
|
||||||
setError("User not authenticated");
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isMounted = false;
|
|
||||||
};
|
|
||||||
}, [repoId, isAuthLoaded, isUserLoaded, userId, user]);
|
|
||||||
|
|
||||||
return { repoData, isLoading, error };
|
|
||||||
}
|
|
||||||
215
apps/deploy-fe/src/hooks/useAuthStatus.tsx
Normal file
215
apps/deploy-fe/src/hooks/useAuthStatus.tsx
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
// 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'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @interface AuthStatus
|
||||||
|
* @description Comprehensive authentication status across all systems
|
||||||
|
*/
|
||||||
|
export interface AuthStatus {
|
||||||
|
// Individual auth systems
|
||||||
|
clerk: {
|
||||||
|
isSignedIn: boolean
|
||||||
|
isLoaded: boolean
|
||||||
|
hasGithubConnected: boolean
|
||||||
|
user: any
|
||||||
|
}
|
||||||
|
wallet: {
|
||||||
|
isConnected: boolean // SIWE authenticated + backend session
|
||||||
|
hasAddress: boolean // Just has wallet address
|
||||||
|
wallet: any
|
||||||
|
}
|
||||||
|
backend: {
|
||||||
|
isConnected: boolean
|
||||||
|
hasGithubAuth: boolean
|
||||||
|
isLoading: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computed status
|
||||||
|
isFullyAuthenticated: boolean
|
||||||
|
isReady: boolean
|
||||||
|
|
||||||
|
// What's missing (for UI feedback)
|
||||||
|
missing: {
|
||||||
|
clerkSignIn: boolean
|
||||||
|
clerkGithub: boolean
|
||||||
|
walletConnection: boolean
|
||||||
|
backendConnection: boolean
|
||||||
|
githubBackendSync: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// Progress (for UI indicators)
|
||||||
|
progress: {
|
||||||
|
completed: number
|
||||||
|
total: number
|
||||||
|
percentage: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @interface AuthActions
|
||||||
|
* @description Available authentication actions
|
||||||
|
*/
|
||||||
|
export interface AuthActions {
|
||||||
|
// Wallet actions
|
||||||
|
connectWallet: () => Promise<void>
|
||||||
|
|
||||||
|
// Combined actions
|
||||||
|
refreshAllStatus: () => Promise<void>
|
||||||
|
checkGithubBackendAuth: () => Promise<boolean>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @hook useAuthStatus
|
||||||
|
* @description Provides comprehensive authentication status and actions
|
||||||
|
* @returns Combined auth status and actions
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
} = 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)
|
||||||
|
|
||||||
|
// Check GitHub backend auth via GraphQL
|
||||||
|
const checkGithubBackendAuth = useCallback(async (): Promise<boolean> => {
|
||||||
|
if (!isBackendConnected) return false
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsCheckingGithubAuth(true)
|
||||||
|
const userData = await gqlClient.getUser()
|
||||||
|
const hasGitHubToken = !!userData.user.gitHubToken
|
||||||
|
setIsGithubBackendAuth(hasGitHubToken)
|
||||||
|
return hasGitHubToken
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking GitHub backend auth:', error)
|
||||||
|
setIsGithubBackendAuth(false)
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
setIsCheckingGithubAuth(false)
|
||||||
|
}
|
||||||
|
}, [isBackendConnected, gqlClient])
|
||||||
|
|
||||||
|
// Check GitHub auth when backend connection changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (isBackendConnected) {
|
||||||
|
checkGithubBackendAuth()
|
||||||
|
} else {
|
||||||
|
setIsGithubBackendAuth(false)
|
||||||
|
}
|
||||||
|
}, [isBackendConnected, checkGithubBackendAuth])
|
||||||
|
|
||||||
|
// 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
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
refreshBackendStatus()
|
||||||
|
}, 1000)
|
||||||
|
return () => clearTimeout(timer)
|
||||||
|
}
|
||||||
|
}, [isWalletSessionActive, refreshBackendStatus])
|
||||||
|
|
||||||
|
// Check if GitHub is connected in Clerk
|
||||||
|
const hasGithubInClerk = user?.externalAccounts?.find(
|
||||||
|
account => account.provider === 'github' || account.verification?.strategy === 'oauth_github'
|
||||||
|
) !== undefined
|
||||||
|
|
||||||
|
// Calculate what's missing
|
||||||
|
const missing = {
|
||||||
|
clerkSignIn: !isSignedIn,
|
||||||
|
clerkGithub: isSignedIn && !hasGithubInClerk,
|
||||||
|
walletConnection: !hasWalletAddress, // Just need wallet address for this step
|
||||||
|
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
|
||||||
|
]
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Determine if ready (all auth systems loaded)
|
||||||
|
const isReady = isClerkLoaded && isUserLoaded && !isBackendLoading && !isCheckingGithubAuth
|
||||||
|
|
||||||
|
// Combined refresh action
|
||||||
|
const refreshAllStatus = async () => {
|
||||||
|
await refreshBackendStatus()
|
||||||
|
await checkGithubBackendAuth()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Individual systems
|
||||||
|
clerk: {
|
||||||
|
isSignedIn,
|
||||||
|
isLoaded: isClerkLoaded && isUserLoaded,
|
||||||
|
hasGithubConnected: hasGithubInClerk,
|
||||||
|
user
|
||||||
|
},
|
||||||
|
wallet: {
|
||||||
|
isConnected: isWalletSessionActive,
|
||||||
|
hasAddress: hasWalletAddress,
|
||||||
|
wallet
|
||||||
|
},
|
||||||
|
backend: {
|
||||||
|
isConnected: isBackendConnected,
|
||||||
|
hasGithubAuth: isGithubBackendAuth,
|
||||||
|
isLoading: isBackendLoading || isCheckingGithubAuth
|
||||||
|
},
|
||||||
|
|
||||||
|
// Computed status
|
||||||
|
isFullyAuthenticated,
|
||||||
|
isReady,
|
||||||
|
|
||||||
|
// Missing items
|
||||||
|
missing,
|
||||||
|
|
||||||
|
// Progress
|
||||||
|
progress: {
|
||||||
|
completed: completedSteps,
|
||||||
|
total: totalSteps,
|
||||||
|
percentage: progressPercentage
|
||||||
|
},
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
connectWallet,
|
||||||
|
refreshAllStatus,
|
||||||
|
checkGithubBackendAuth
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,94 +1,131 @@
|
|||||||
|
// src/hooks/useDeployment.tsx
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useGQLClient } from '@/context'
|
import { useGQLClient } from '@/context'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
interface DeploymentConfig {
|
// Define the structure of deployment configuration
|
||||||
repositoryUrl: string;
|
export interface DeploymentConfig {
|
||||||
branch: string;
|
projectId?: string
|
||||||
environmentVariables?: Record<string, string>;
|
organizationSlug: string
|
||||||
projectName?: string;
|
repository: string
|
||||||
customDomain?: string;
|
branch: string
|
||||||
|
name: string
|
||||||
|
environmentVariables?: Array<{
|
||||||
|
key: string
|
||||||
|
value: string
|
||||||
|
environments: string[]
|
||||||
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DeploymentResult {
|
// Define the structure of deployment result
|
||||||
id: string;
|
export interface DeploymentResult {
|
||||||
url: string;
|
id: string
|
||||||
status: 'pending' | 'building' | 'ready' | 'error';
|
url?: string
|
||||||
|
status: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDeployment() {
|
export function useDeployment() {
|
||||||
const [isDeploying, setIsDeploying] = useState(false)
|
const [isDeploying, setIsDeploying] = useState(false)
|
||||||
const [deploymentResult, setDeploymentResult] = useState<DeploymentResult | null>(null)
|
const [deploymentResult, setDeploymentResult] = useState<DeploymentResult | null>(null)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
const gqlClient = useGQLClient()
|
const gqlClient = useGQLClient()
|
||||||
|
|
||||||
|
// Function to create a new project and deploy it
|
||||||
const deployRepository = async (config: DeploymentConfig): Promise<DeploymentResult> => {
|
const deployRepository = async (config: DeploymentConfig): Promise<DeploymentResult> => {
|
||||||
setIsDeploying(true)
|
setIsDeploying(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// This is a placeholder query - you'll need to replace it with the actual GraphQL mutation
|
console.log('🚀 Starting repository deployment:', config)
|
||||||
// based on the snowballtools-base API schema
|
|
||||||
const result = await gqlClient.mutate({
|
// Use the addProject mutation from your existing GraphQL client
|
||||||
mutation: `
|
const projectResult = await gqlClient.addProject(
|
||||||
mutation CreateDeployment($input: CreateDeploymentInput!) {
|
config.organizationSlug,
|
||||||
createDeployment(input: $input) {
|
{
|
||||||
id
|
name: config.name,
|
||||||
url
|
repository: config.repository,
|
||||||
status
|
prodBranch: config.branch,
|
||||||
}
|
template: 'webapp', // Default template
|
||||||
}
|
paymentAddress: "0x1ac42F4A25Ae0137d10a825a2e33e32de0F6B57E", // Should come from wallet
|
||||||
`,
|
txHash: "0x0000000000000000000000000000000000000000000000000000000000000000" // Placeholder
|
||||||
variables: {
|
},
|
||||||
input: {
|
undefined, // lrn - will be handled in configure step
|
||||||
repositoryUrl: config.repositoryUrl,
|
undefined, // auctionParams - will be handled in configure step
|
||||||
branch: config.branch,
|
config.environmentVariables || []
|
||||||
environmentVariables: config.environmentVariables || {},
|
)
|
||||||
projectName: config.projectName,
|
|
||||||
customDomain: config.customDomain
|
if (!projectResult.addProject?.id) {
|
||||||
}
|
throw new Error('Failed to create project')
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
toast.success('Project created successfully!')
|
||||||
|
|
||||||
|
// Wait a moment for deployment to start
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||||
|
|
||||||
const deployment = result.data.createDeployment
|
// Get the newly created project to find its deployment
|
||||||
setDeploymentResult(deployment)
|
const projectData = await gqlClient.getProject(projectResult.addProject.id)
|
||||||
|
|
||||||
|
// Find the most recent deployment
|
||||||
|
const deployment = projectData.project?.deployments?.[0]
|
||||||
|
|
||||||
|
const deploymentResult: DeploymentResult = {
|
||||||
|
id: deployment?.id || projectResult.addProject.id,
|
||||||
|
url: deployment?.applicationDeploymentRecordData?.url,
|
||||||
|
status: deployment?.status || 'Building'
|
||||||
|
}
|
||||||
|
|
||||||
|
setDeploymentResult(deploymentResult)
|
||||||
toast.success('Deployment initiated successfully')
|
toast.success('Deployment initiated successfully')
|
||||||
return deployment
|
return deploymentResult
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Deployment failed:', error)
|
console.error('Deployment failed:', error)
|
||||||
toast.error('Failed to deploy repository')
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
setError(errorMessage)
|
||||||
|
toast.error(`Failed to deploy repository: ${errorMessage}`)
|
||||||
throw error
|
throw error
|
||||||
} finally {
|
} finally {
|
||||||
setIsDeploying(false)
|
setIsDeploying(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDeploymentStatus = async (deploymentId: string): Promise<string> => {
|
// Function to check the status of a deployment
|
||||||
|
const getDeploymentStatus = async (projectId: string) => {
|
||||||
try {
|
try {
|
||||||
const result = await gqlClient.query({
|
const result = await gqlClient.getProject(projectId)
|
||||||
query: `
|
return result.project?.deployments?.[0]?.status
|
||||||
query GetDeploymentStatus($id: ID!) {
|
|
||||||
deployment(id: $id) {
|
|
||||||
id
|
|
||||||
status
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
variables: {
|
|
||||||
id: deploymentId
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return result.data.deployment.status
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get deployment status:', error)
|
console.error('Failed to get deployment status:', error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function to get all deployments for a project
|
||||||
|
const getDeployments = async (projectId: string) => {
|
||||||
|
try {
|
||||||
|
return await gqlClient.getDeployments(projectId)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get deployments:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
setDeploymentResult(null)
|
||||||
|
setError(null)
|
||||||
|
setIsDeploying(false)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
deployRepository,
|
deployRepository,
|
||||||
getDeploymentStatus,
|
getDeploymentStatus,
|
||||||
|
getDeployments,
|
||||||
isDeploying,
|
isDeploying,
|
||||||
deploymentResult
|
deploymentResult,
|
||||||
|
error,
|
||||||
|
reset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -62,13 +62,6 @@ export function useRepoData(repoId: string): UseRepoDataReturn {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Final fallback to hardcoded token - ONLY for development
|
|
||||||
if (!token && process.env.NODE_ENV === 'development') {
|
|
||||||
// Store your token in .env.local instead, using this only as last resort
|
|
||||||
token = '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';
|
|
||||||
console.warn('Using hardcoded token - INSECURE. Use environment variables instead.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create Octokit instance with whatever token we found
|
// Create Octokit instance with whatever token we found
|
||||||
if (token) {
|
if (token) {
|
||||||
setOctokit(new Octokit({ auth: token }));
|
setOctokit(new Octokit({ auth: token }));
|
||||||
|
|||||||
186
apps/deploy-fe/src/hooks/useTemplate.tsx
Normal file
186
apps/deploy-fe/src/hooks/useTemplate.tsx
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
// src/hooks/useTemplateDeployment.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'
|
||||||
|
|
||||||
|
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 useTemplateDeployment() {
|
||||||
|
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 deployTemplate = async (config: TemplateDeploymentConfig): Promise<TemplateDeploymentResult> => {
|
||||||
|
setIsDeploying(true)
|
||||||
|
setError(null)
|
||||||
|
setDeploymentResult(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('🚀 Starting 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')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
console.log('🔍 Template parsing details:', {
|
||||||
|
originalTemplate: config.template.repoFullName,
|
||||||
|
parsedOwner: templateOwner,
|
||||||
|
parsedRepo: templateRepo,
|
||||||
|
templateId: config.template.id,
|
||||||
|
templateName: config.template.name
|
||||||
|
})
|
||||||
|
|
||||||
|
const requestData = {
|
||||||
|
templateOwner,
|
||||||
|
templateRepo,
|
||||||
|
owner: githubUsername, // Use the authenticated GitHub username
|
||||||
|
name: config.projectName,
|
||||||
|
isPrivate: false,
|
||||||
|
paymentAddress: wallet.address,
|
||||||
|
txHash: "0x0000000000000000000000000000000000000000000000000000000000000000" // Placeholder - will be updated if payment is required
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔍 Request data being sent to backend:', requestData)
|
||||||
|
console.log('🔍 Organization slug:', config.organizationSlug)
|
||||||
|
console.log('🔍 Deployer LRN:', config.deployerLrn)
|
||||||
|
console.log('🔍 Environment variables:', config.environmentVariables)
|
||||||
|
|
||||||
|
toast.info('Creating repository from template...')
|
||||||
|
|
||||||
|
// Use the backend's addProjectFromTemplate method
|
||||||
|
const projectResult = await gqlClient.addProjectFromTemplate(
|
||||||
|
config.organizationSlug,
|
||||||
|
requestData,
|
||||||
|
config.deployerLrn, // deployer LRN for direct deployment
|
||||||
|
undefined, // auctionParams - not used for LRN deployments
|
||||||
|
config.environmentVariables || []
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log('🔍 Backend response:', projectResult)
|
||||||
|
|
||||||
|
if (!projectResult.addProjectFromTemplate?.id) {
|
||||||
|
throw new Error('Failed to create project from template')
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success('Repository created from template!')
|
||||||
|
|
||||||
|
// Wait for deployment to start
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||||
|
|
||||||
|
// Get project with deployment info
|
||||||
|
const projectData = await gqlClient.getProject(projectResult.addProjectFromTemplate.id)
|
||||||
|
|
||||||
|
console.log('🔍 Project data after creation:', projectData)
|
||||||
|
|
||||||
|
if (!projectData.project) {
|
||||||
|
throw new Error('Project not found after creation')
|
||||||
|
}
|
||||||
|
|
||||||
|
const deployment = projectData.project.deployments?.[0]
|
||||||
|
|
||||||
|
const result: TemplateDeploymentResult = {
|
||||||
|
projectId: projectResult.addProjectFromTemplate.id,
|
||||||
|
repositoryUrl: `https://github.com/${projectData.project.repository}`,
|
||||||
|
deploymentUrl: deployment?.applicationDeploymentRecordData?.url,
|
||||||
|
deploymentId: deployment?.id
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔍 Final deployment result:', result)
|
||||||
|
|
||||||
|
setDeploymentResult(result)
|
||||||
|
toast.success('Template deployed successfully!')
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
} 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 getTemplateInfo = async (templateRepo: string) => {
|
||||||
|
try {
|
||||||
|
// This would fetch template information if needed
|
||||||
|
// For now, we can just return the repo name
|
||||||
|
return {
|
||||||
|
name: templateRepo,
|
||||||
|
description: `Template from ${templateRepo}`
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching template info:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
setDeploymentResult(null)
|
||||||
|
setError(null)
|
||||||
|
setIsDeploying(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
deployTemplate,
|
||||||
|
getTemplateInfo,
|
||||||
|
isDeploying,
|
||||||
|
deploymentResult,
|
||||||
|
error,
|
||||||
|
reset
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -34,5 +34,9 @@
|
|||||||
"packageManager": "pnpm@10.5.1",
|
"packageManager": "pnpm@10.5.1",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.14.0"
|
"node": ">=22.14.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ethers": "^6.14.1",
|
||||||
|
"siwe": "^3.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
90
pnpm-lock.yaml
generated
90
pnpm-lock.yaml
generated
@ -7,6 +7,13 @@ settings:
|
|||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
|
dependencies:
|
||||||
|
ethers:
|
||||||
|
specifier: ^6.14.1
|
||||||
|
version: 6.14.1
|
||||||
|
siwe:
|
||||||
|
specifier: ^3.0.0
|
||||||
|
version: 3.0.0(ethers@6.14.1)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@biomejs/biome':
|
'@biomejs/biome':
|
||||||
specifier: ^1.9.4
|
specifier: ^1.9.4
|
||||||
@ -112,7 +119,7 @@ importers:
|
|||||||
version: 7.7.1
|
version: 7.7.1
|
||||||
siwe:
|
siwe:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.0.0(ethers@5.8.0)
|
version: 3.0.0(ethers@6.14.1)
|
||||||
toml:
|
toml:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.0.0
|
version: 3.0.0
|
||||||
@ -320,7 +327,7 @@ importers:
|
|||||||
version: 2.15.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
version: 2.15.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
siwe:
|
siwe:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.0.0(ethers@5.8.0)
|
version: 3.0.0(ethers@6.14.1)
|
||||||
sonner:
|
sonner:
|
||||||
specifier: ^2.0.1
|
specifier: ^2.0.1
|
||||||
version: 2.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
version: 2.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
@ -399,7 +406,7 @@ importers:
|
|||||||
version: 1.9.4
|
version: 1.9.4
|
||||||
'@biomejs/monorepo':
|
'@biomejs/monorepo':
|
||||||
specifier: github:biomejs/biome
|
specifier: github:biomejs/biome
|
||||||
version: https://codeload.github.com/biomejs/biome/tar.gz/7e51cd1e3139c46b4b45eab71936d1641230a566
|
version: https://codeload.github.com/biomejs/biome/tar.gz/a82a1f2e50c5c9b22cc696960bc167631d1de455
|
||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
specifier: ^4.1.2
|
specifier: ^4.1.2
|
||||||
version: 4.1.3(react-hook-form@7.54.2(react@19.0.0))
|
version: 4.1.3(react-hook-form@7.54.2(react@19.0.0))
|
||||||
@ -575,6 +582,9 @@ importers:
|
|||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
|
'@adraffy/ens-normalize@1.10.1':
|
||||||
|
resolution: {integrity: sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==}
|
||||||
|
|
||||||
'@alloc/quick-lru@5.2.0':
|
'@alloc/quick-lru@5.2.0':
|
||||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -720,9 +730,10 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@biomejs/monorepo@https://codeload.github.com/biomejs/biome/tar.gz/7e51cd1e3139c46b4b45eab71936d1641230a566':
|
'@biomejs/monorepo@https://codeload.github.com/biomejs/biome/tar.gz/a82a1f2e50c5c9b22cc696960bc167631d1de455':
|
||||||
resolution: {tarball: https://codeload.github.com/biomejs/biome/tar.gz/7e51cd1e3139c46b4b45eab71936d1641230a566}
|
resolution: {tarball: https://codeload.github.com/biomejs/biome/tar.gz/a82a1f2e50c5c9b22cc696960bc167631d1de455}
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
|
engines: {pnpm: 10.8.1}
|
||||||
|
|
||||||
'@cerc-io/laconic-registry-cli@0.2.10':
|
'@cerc-io/laconic-registry-cli@0.2.10':
|
||||||
resolution: {integrity: sha512-rwrZhFgYZiMh2k+9E/aiyRhFLApydRUwclATb0f6hsFAkxARuaibHsJNy7eF2N/AQ4d6HAvpkXACJoVrGOppmw==, tarball: https://git.vdb.to/api/packages/cerc-io/npm/%40cerc-io%2Flaconic-registry-cli/-/0.2.10/laconic-registry-cli-0.2.10.tgz}
|
resolution: {integrity: sha512-rwrZhFgYZiMh2k+9E/aiyRhFLApydRUwclATb0f6hsFAkxARuaibHsJNy7eF2N/AQ4d6HAvpkXACJoVrGOppmw==, tarball: https://git.vdb.to/api/packages/cerc-io/npm/%40cerc-io%2Flaconic-registry-cli/-/0.2.10/laconic-registry-cli-0.2.10.tgz}
|
||||||
@ -1392,10 +1403,17 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@noble/curves@1.2.0':
|
||||||
|
resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==}
|
||||||
|
|
||||||
'@noble/curves@1.8.1':
|
'@noble/curves@1.8.1':
|
||||||
resolution: {integrity: sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==}
|
resolution: {integrity: sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==}
|
||||||
engines: {node: ^14.21.3 || >=16}
|
engines: {node: ^14.21.3 || >=16}
|
||||||
|
|
||||||
|
'@noble/hashes@1.3.2':
|
||||||
|
resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==}
|
||||||
|
engines: {node: '>= 16'}
|
||||||
|
|
||||||
'@noble/hashes@1.7.1':
|
'@noble/hashes@1.7.1':
|
||||||
resolution: {integrity: sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==}
|
resolution: {integrity: sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==}
|
||||||
engines: {node: ^14.21.3 || >=16}
|
engines: {node: ^14.21.3 || >=16}
|
||||||
@ -2615,6 +2633,9 @@ packages:
|
|||||||
'@types/node@22.13.9':
|
'@types/node@22.13.9':
|
||||||
resolution: {integrity: sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==}
|
resolution: {integrity: sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==}
|
||||||
|
|
||||||
|
'@types/node@22.7.5':
|
||||||
|
resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==}
|
||||||
|
|
||||||
'@types/pbkdf2@3.1.2':
|
'@types/pbkdf2@3.1.2':
|
||||||
resolution: {integrity: sha512-uRwJqmiXmh9++aSu1VNEn3iIxWOhd8AHXNSdlaLfdAAdSTY9jYVeGWnzejM3dvrkbqE3/hyQkQQ29IFATEGlew==}
|
resolution: {integrity: sha512-uRwJqmiXmh9++aSu1VNEn3iIxWOhd8AHXNSdlaLfdAAdSTY9jYVeGWnzejM3dvrkbqE3/hyQkQQ29IFATEGlew==}
|
||||||
|
|
||||||
@ -2691,6 +2712,9 @@ packages:
|
|||||||
aes-js@3.0.0:
|
aes-js@3.0.0:
|
||||||
resolution: {integrity: sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==}
|
resolution: {integrity: sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==}
|
||||||
|
|
||||||
|
aes-js@4.0.0-beta.5:
|
||||||
|
resolution: {integrity: sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==}
|
||||||
|
|
||||||
agent-base@7.1.3:
|
agent-base@7.1.3:
|
||||||
resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==}
|
resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==}
|
||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
@ -3504,6 +3528,10 @@ packages:
|
|||||||
ethers@5.8.0:
|
ethers@5.8.0:
|
||||||
resolution: {integrity: sha512-DUq+7fHrCg1aPDFCHx6UIPb3nmt2XMpM7Y/g2gLhsl3lIBqeAfOJIl1qEvRf2uq3BiKxmh6Fh5pfp2ieyek7Kg==}
|
resolution: {integrity: sha512-DUq+7fHrCg1aPDFCHx6UIPb3nmt2XMpM7Y/g2gLhsl3lIBqeAfOJIl1qEvRf2uq3BiKxmh6Fh5pfp2ieyek7Kg==}
|
||||||
|
|
||||||
|
ethers@6.14.1:
|
||||||
|
resolution: {integrity: sha512-JnFiPFi3sK2Z6y7jZ3qrafDMwiXmU+6cNZ0M+kPq+mTy9skqEzwqAdFW3nb/em2xjlIVXX6Lz8ID6i3LmS4+fQ==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
ethjs-util@0.1.6:
|
ethjs-util@0.1.6:
|
||||||
resolution: {integrity: sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w==}
|
resolution: {integrity: sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w==}
|
||||||
engines: {node: '>=6.5.0', npm: '>=3'}
|
engines: {node: '>=6.5.0', npm: '>=3'}
|
||||||
@ -5259,6 +5287,9 @@ packages:
|
|||||||
tslib@2.4.1:
|
tslib@2.4.1:
|
||||||
resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==}
|
resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==}
|
||||||
|
|
||||||
|
tslib@2.7.0:
|
||||||
|
resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==}
|
||||||
|
|
||||||
tslib@2.8.1:
|
tslib@2.8.1:
|
||||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
@ -5592,6 +5623,18 @@ packages:
|
|||||||
utf-8-validate:
|
utf-8-validate:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
ws@8.17.1:
|
||||||
|
resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
bufferutil: ^4.0.1
|
||||||
|
utf-8-validate: '>=5.0.2'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
bufferutil:
|
||||||
|
optional: true
|
||||||
|
utf-8-validate:
|
||||||
|
optional: true
|
||||||
|
|
||||||
ws@8.18.0:
|
ws@8.18.0:
|
||||||
resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
|
resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
@ -5677,6 +5720,8 @@ packages:
|
|||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
|
'@adraffy/ens-normalize@1.10.1': {}
|
||||||
|
|
||||||
'@alloc/quick-lru@5.2.0': {}
|
'@alloc/quick-lru@5.2.0': {}
|
||||||
|
|
||||||
'@apollo/client@3.13.3(@types/react@18.3.0)(graphql@16.10.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
|
'@apollo/client@3.13.3(@types/react@18.3.0)(graphql@16.10.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
|
||||||
@ -5826,7 +5871,7 @@ snapshots:
|
|||||||
'@biomejs/cli-win32-x64@1.9.4':
|
'@biomejs/cli-win32-x64@1.9.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@biomejs/monorepo@https://codeload.github.com/biomejs/biome/tar.gz/7e51cd1e3139c46b4b45eab71936d1641230a566': {}
|
'@biomejs/monorepo@https://codeload.github.com/biomejs/biome/tar.gz/a82a1f2e50c5c9b22cc696960bc167631d1de455': {}
|
||||||
|
|
||||||
'@cerc-io/laconic-registry-cli@0.2.10':
|
'@cerc-io/laconic-registry-cli@0.2.10':
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -6775,10 +6820,16 @@ snapshots:
|
|||||||
'@next/swc-win32-x64-msvc@15.2.1':
|
'@next/swc-win32-x64-msvc@15.2.1':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@noble/curves@1.2.0':
|
||||||
|
dependencies:
|
||||||
|
'@noble/hashes': 1.3.2
|
||||||
|
|
||||||
'@noble/curves@1.8.1':
|
'@noble/curves@1.8.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@noble/hashes': 1.7.1
|
'@noble/hashes': 1.7.1
|
||||||
|
|
||||||
|
'@noble/hashes@1.3.2': {}
|
||||||
|
|
||||||
'@noble/hashes@1.7.1': {}
|
'@noble/hashes@1.7.1': {}
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
@ -8112,6 +8163,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.20.0
|
undici-types: 6.20.0
|
||||||
|
|
||||||
|
'@types/node@22.7.5':
|
||||||
|
dependencies:
|
||||||
|
undici-types: 6.19.8
|
||||||
|
|
||||||
'@types/pbkdf2@3.1.2':
|
'@types/pbkdf2@3.1.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.17.23
|
'@types/node': 20.17.23
|
||||||
@ -8189,6 +8244,8 @@ snapshots:
|
|||||||
|
|
||||||
aes-js@3.0.0: {}
|
aes-js@3.0.0: {}
|
||||||
|
|
||||||
|
aes-js@4.0.0-beta.5: {}
|
||||||
|
|
||||||
agent-base@7.1.3: {}
|
agent-base@7.1.3: {}
|
||||||
|
|
||||||
aggregate-error@3.1.0:
|
aggregate-error@3.1.0:
|
||||||
@ -9143,6 +9200,19 @@ snapshots:
|
|||||||
- bufferutil
|
- bufferutil
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
|
||||||
|
ethers@6.14.1:
|
||||||
|
dependencies:
|
||||||
|
'@adraffy/ens-normalize': 1.10.1
|
||||||
|
'@noble/curves': 1.2.0
|
||||||
|
'@noble/hashes': 1.3.2
|
||||||
|
'@types/node': 22.7.5
|
||||||
|
aes-js: 4.0.0-beta.5
|
||||||
|
tslib: 2.7.0
|
||||||
|
ws: 8.17.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
ethjs-util@0.1.6:
|
ethjs-util@0.1.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-hex-prefixed: 1.0.0
|
is-hex-prefixed: 1.0.0
|
||||||
@ -10727,11 +10797,11 @@ snapshots:
|
|||||||
is-arrayish: 0.3.2
|
is-arrayish: 0.3.2
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
siwe@3.0.0(ethers@5.8.0):
|
siwe@3.0.0(ethers@6.14.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@spruceid/siwe-parser': 3.0.0
|
'@spruceid/siwe-parser': 3.0.0
|
||||||
'@stablelib/random': 1.0.2
|
'@stablelib/random': 1.0.2
|
||||||
ethers: 5.8.0
|
ethers: 6.14.1
|
||||||
|
|
||||||
slash@3.0.0: {}
|
slash@3.0.0: {}
|
||||||
|
|
||||||
@ -11057,6 +11127,8 @@ snapshots:
|
|||||||
|
|
||||||
tslib@2.4.1: {}
|
tslib@2.4.1: {}
|
||||||
|
|
||||||
|
tslib@2.7.0: {}
|
||||||
|
|
||||||
tslib@2.8.1: {}
|
tslib@2.8.1: {}
|
||||||
|
|
||||||
tsscmp@1.0.6: {}
|
tsscmp@1.0.6: {}
|
||||||
@ -11324,6 +11396,8 @@ snapshots:
|
|||||||
|
|
||||||
ws@7.5.10: {}
|
ws@7.5.10: {}
|
||||||
|
|
||||||
|
ws@8.17.1: {}
|
||||||
|
|
||||||
ws@8.18.0: {}
|
ws@8.18.0: {}
|
||||||
|
|
||||||
xss@1.0.15:
|
xss@1.0.15:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user