Files
moreminimore-vibe/src/components/GitHubConnector.tsx
2025-04-14 23:15:58 -07:00

375 lines
13 KiB
TypeScript

import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Github } from "lucide-react";
import { IpcClient } from "@/ipc/ipc_client";
import { useSettings } from "@/hooks/useSettings";
import { useLoadApp } from "@/hooks/useLoadApp";
interface GitHubConnectorProps {
appId: number | null;
folderName: string;
}
export function GitHubConnector({ appId, folderName }: GitHubConnectorProps) {
// --- GitHub Device Flow State ---
const { app, refreshApp } = useLoadApp(appId);
const { settings, refreshSettings } = useSettings();
const [githubUserCode, setGithubUserCode] = useState<string | null>(null);
const [githubVerificationUri, setGithubVerificationUri] = useState<
string | null
>(null);
const [githubError, setGithubError] = useState<string | null>(null);
const [isConnectingToGithub, setIsConnectingToGithub] = useState(false);
const [githubStatusMessage, setGithubStatusMessage] = useState<string | null>(
null
);
// --- ---
const handleConnectToGithub = async () => {
if (!appId) return;
setIsConnectingToGithub(true);
setGithubError(null);
setGithubUserCode(null);
setGithubVerificationUri(null);
setGithubStatusMessage("Requesting device code from GitHub...");
// Send IPC message to main process to start the flow
IpcClient.getInstance().startGithubDeviceFlow(appId);
};
useEffect(() => {
if (!appId) return; // Don't set up listeners if appId is null initially
const cleanupFunctions: (() => void)[] = [];
// Listener for updates (user code, verification uri, status messages)
const removeUpdateListener =
IpcClient.getInstance().onGithubDeviceFlowUpdate((data) => {
console.log("Received github:flow-update", data);
if (data.userCode) {
setGithubUserCode(data.userCode);
}
if (data.verificationUri) {
setGithubVerificationUri(data.verificationUri);
}
if (data.message) {
setGithubStatusMessage(data.message);
}
setGithubError(null); // Clear previous errors on new update
if (!data.userCode && !data.verificationUri && data.message) {
// Likely just a status message, keep connecting state
setIsConnectingToGithub(true);
}
if (data.userCode && data.verificationUri) {
setIsConnectingToGithub(true); // Still connecting until success/error
}
});
cleanupFunctions.push(removeUpdateListener);
// Listener for success
const removeSuccessListener =
IpcClient.getInstance().onGithubDeviceFlowSuccess((data) => {
console.log("Received github:flow-success", data);
setGithubStatusMessage("Successfully connected to GitHub!");
setGithubUserCode(null); // Clear user-facing info
setGithubVerificationUri(null);
setGithubError(null);
setIsConnectingToGithub(false);
refreshSettings();
// TODO: Maybe update parent UI to show "Connected" state or trigger next action
});
cleanupFunctions.push(removeSuccessListener);
// Listener for errors
const removeErrorListener = IpcClient.getInstance().onGithubDeviceFlowError(
(data) => {
console.log("Received github:flow-error", data);
setGithubError(data.error || "An unknown error occurred.");
setGithubStatusMessage(null);
setGithubUserCode(null);
setGithubVerificationUri(null);
setIsConnectingToGithub(false);
}
);
cleanupFunctions.push(removeErrorListener);
// Cleanup function to remove all listeners when component unmounts or appId changes
return () => {
cleanupFunctions.forEach((cleanup) => cleanup());
// Optional: Send a message to main process to cancel polling if component unmounts
// Only cancel if we were actually connecting for this specific appId
// IpcClient.getInstance().cancelGithubDeviceFlow(appId);
// Reset state when appId changes or component unmounts
setGithubUserCode(null);
setGithubVerificationUri(null);
setGithubError(null);
setIsConnectingToGithub(false);
setGithubStatusMessage(null);
};
}, [appId]); // Re-run effect if appId changes
// --- Create Repo State ---
const [repoName, setRepoName] = useState(folderName);
const [repoAvailable, setRepoAvailable] = useState<boolean | null>(null);
const [repoCheckError, setRepoCheckError] = useState<string | null>(null);
const [isCheckingRepo, setIsCheckingRepo] = useState(false);
const [isCreatingRepo, setIsCreatingRepo] = useState(false);
const [createRepoError, setCreateRepoError] = useState<string | null>(null);
const [createRepoSuccess, setCreateRepoSuccess] = useState<boolean>(false);
// --- Sync to GitHub State ---
const [isSyncing, setIsSyncing] = useState(false);
const [syncError, setSyncError] = useState<string | null>(null);
const [syncSuccess, setSyncSuccess] = useState<boolean>(false);
// Assume org is the authenticated user for now (could add org input later)
// TODO: After device flow, fetch and store the GitHub username/org in settings for use here
const githubOrg = ""; // Use empty string for now (GitHub API will default to the authenticated user)
const handleRepoNameBlur = async () => {
setRepoCheckError(null);
setRepoAvailable(null);
if (!repoName) return;
setIsCheckingRepo(true);
try {
const result = await IpcClient.getInstance().checkGithubRepoAvailable(
githubOrg,
repoName
);
setRepoAvailable(result.available);
if (!result.available) {
setRepoCheckError(result.error || "Repository name is not available.");
}
} catch (err: any) {
setRepoCheckError(err.message || "Failed to check repo availability.");
} finally {
setIsCheckingRepo(false);
}
};
const handleCreateRepo = async (e: React.FormEvent) => {
e.preventDefault();
setCreateRepoError(null);
setIsCreatingRepo(true);
setCreateRepoSuccess(false);
try {
const result = await IpcClient.getInstance().createGithubRepo(
githubOrg,
repoName,
appId!
);
if (result.success) {
setCreateRepoSuccess(true);
setRepoCheckError(null);
refreshApp();
} else {
setCreateRepoError(result.error || "Failed to create repository.");
}
} catch (err: any) {
setCreateRepoError(err.message || "Failed to create repository.");
} finally {
setIsCreatingRepo(false);
}
};
if (!settings?.githubAccessToken) {
return (
<div className="mt-4 w-full">
{" "}
<Button
onClick={handleConnectToGithub}
className="cursor-pointer w-full py-6 flex justify-center items-center gap-2 text-lg"
size="lg"
variant="outline"
disabled={isConnectingToGithub || !appId} // Also disable if appId is null
>
Connect to GitHub
<Github className="h-5 w-5" />
{isConnectingToGithub && (
<svg
className="animate-spin h-5 w-5 ml-2"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
)}
</Button>
{/* GitHub Connection Status/Instructions */}
{(githubUserCode || githubStatusMessage || githubError) && (
<div className="mt-6 p-4 border rounded-md bg-gray-50 dark:bg-gray-700/50 border-gray-200 dark:border-gray-600">
<h4 className="font-medium mb-2">GitHub Connection</h4>
{githubError && (
<p className="text-red-600 dark:text-red-400 mb-2">
Error: {githubError}
</p>
)}
{githubUserCode && githubVerificationUri && (
<div className="mb-2">
<p>
1. Go to:
<a
href={githubVerificationUri} // Make it a direct link
onClick={(e) => {
e.preventDefault();
IpcClient.getInstance().openExternalUrl(
githubVerificationUri
);
}}
target="_blank"
rel="noopener noreferrer"
className="ml-1 text-blue-600 hover:underline dark:text-blue-400"
>
{githubVerificationUri}
</a>
</p>
<p>
2. Enter code:
<strong className="ml-1 font-mono text-lg tracking-wider bg-gray-200 dark:bg-gray-600 px-2 py-0.5 rounded">
{githubUserCode}
</strong>
</p>
</div>
)}
{githubStatusMessage && (
<p className="text-sm text-gray-600 dark:text-gray-300">
{githubStatusMessage}
</p>
)}
</div>
)}
</div>
);
}
if (app?.githubOrg && app?.githubRepo) {
const handleSyncToGithub = async () => {
setIsSyncing(true);
setSyncError(null);
setSyncSuccess(false);
try {
const result = await IpcClient.getInstance().syncGithubRepo(appId!);
if (result.success) {
setSyncSuccess(true);
} else {
setSyncError(result.error || "Failed to sync to GitHub.");
}
} catch (err: any) {
setSyncError(err.message || "Failed to sync to GitHub.");
} finally {
setIsSyncing(false);
}
};
return (
<div className="mt-4 w-full border border-gray-200 rounded-md p-4">
<p>Connected to GitHub Repo:</p>
<a
onClick={(e) => {
e.preventDefault();
IpcClient.getInstance().openExternalUrl(
`https://github.com/${app.githubOrg}/${app.githubRepo}`
);
}}
className="cursor-pointer text-blue-600 hover:underline dark:text-blue-400"
target="_blank"
rel="noopener noreferrer"
>
{app.githubOrg}/{app.githubRepo}
</a>
<div className="mt-2">
<Button onClick={handleSyncToGithub} disabled={isSyncing}>
{isSyncing ? (
<>
<svg
className="animate-spin h-5 w-5 mr-2 inline"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
style={{ display: "inline" }}
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Syncing...
</>
) : (
"Sync to GitHub"
)}
</Button>
</div>
{syncError && <p className="text-red-600 mt-2">{syncError}</p>}
{syncSuccess && (
<p className="text-green-600 mt-2">Successfully pushed to GitHub!</p>
)}
</div>
);
} else {
return (
<div className="mt-4 w-full border border-gray-200 rounded-md p-4">
<p>Set up your GitHub repo</p>
<form className="mt-4 space-y-2" onSubmit={handleCreateRepo}>
<label className="block text-sm font-medium">Repository Name</label>
<input
className="w-full border rounded px-2 py-1"
value={repoName}
onChange={(e) => {
setRepoName(e.target.value);
setRepoAvailable(null);
setRepoCheckError(null);
}}
onBlur={handleRepoNameBlur}
disabled={isCreatingRepo}
/>
{isCheckingRepo && (
<p className="text-xs text-gray-500">Checking availability...</p>
)}
{repoAvailable === true && (
<p className="text-xs text-green-600">
Repository name is available!
</p>
)}
{repoAvailable === false && (
<p className="text-xs text-red-600">{repoCheckError}</p>
)}
<Button
type="submit"
disabled={isCreatingRepo || repoAvailable === false || !repoName}
>
{isCreatingRepo ? "Creating..." : "Create Repo"}
</Button>
</form>
{createRepoError && (
<p className="text-red-600 mt-2">{createRepoError}</p>
)}
{createRepoSuccess && (
<p className="text-green-600 mt-2">Repository created and linked!</p>
)}
</div>
);
}
}