Files
zopu-code/apps/web/src/components/projects/provider-chips.tsx
T
-Puter ce16813fbc feat(github): live repo search with /user/repos pagination and connection gear UI
- searchGithubRepositories action fetches up to 300 repos via paginated
  /user/repos, filters by name, upserts results into gitRepositories so
  createProjectFromRepository works unchanged; token never leaves backend
- connectGithub now persists grantedScopesJson from getAccessToken().scopes
- listProviderAccounts and list queries return grantedScopesJson
- Gear/settings popover on connected GitHub chip shows scopes and triggers
  linkSocial with expanded scope set [repo, read:org, read:user, user:email]
- RepositorySelector merges debounced live search results with synced snapshot
- Restored CONVEX_SITE_URL env var (removed in prior commit but still used)
2026-08-01 20:33:55 +05:30

167 lines
4.5 KiB
TypeScript

import { authClient } from "@code/auth/web";
import { Button } from "@code/ui/components/button";
import {
CheckCircle2,
GitBranch,
LoaderCircle,
Server,
XCircle,
} from "lucide-react";
import { useState } from "react";
import { GithubConnectionSettings } from "./github-connection-settings";
import { PuterConnectForm } from "./puter-connect-form";
export interface GitProviderAccountOption {
readonly externalUsername: string;
readonly grantedScopesJson?: string;
readonly id: string;
readonly provider: "github" | "gitea";
readonly serverUrl: string;
readonly status:
| "pending-auth"
| "active"
| "reauth-required"
| "revoked"
| "unavailable";
}
interface ProviderChipsProps {
readonly accounts: readonly GitProviderAccountOption[] | undefined;
}
interface ProviderChipProps {
readonly account: GitProviderAccountOption | undefined;
readonly icon: React.ElementType;
readonly label: string;
readonly loading: boolean;
readonly onClick: () => void;
}
const isHealthy = (status: GitProviderAccountOption["status"]) =>
status === "active";
const ProviderStatusIcon = ({ connected }: { readonly connected: boolean }) =>
connected ? (
<CheckCircle2 className="size-3 text-emerald-400" />
) : (
<XCircle className="size-3 text-amber-500" />
);
const ProviderChip = ({
account,
icon: Icon,
label,
loading,
onClick,
}: ProviderChipProps) => {
const connected = account !== undefined && isHealthy(account.status);
return (
<Button
aria-pressed={connected}
className={
connected
? "h-8 gap-1.5 rounded border-[#d7d3c7] bg-[#f5f3eb] px-3 text-[#20201d] hover:bg-[#f5f3eb]"
: "h-8 gap-1.5 rounded border-[#c9c5b9] bg-[#fffefa] px-3 text-[#68665e] hover:bg-[#f5f3eb]"
}
disabled={loading || connected}
onClick={onClick}
size="sm"
type="button"
variant="outline"
>
{loading ? (
<LoaderCircle className="size-3.5 animate-spin" />
) : (
<Icon className="size-3.5 text-[#747168]" />
)}
<span className="text-xs font-medium">
{connected ? account.externalUsername : `Connect ${label}`}
</span>
{account === undefined ? null : (
<ProviderStatusIcon connected={connected} />
)}
</Button>
);
};
export const ProviderChips = ({ accounts }: ProviderChipsProps) => {
const [pendingProvider, setPendingProvider] = useState<
"github" | "gitea" | undefined
>();
const [showPuterForm, setShowPuterForm] = useState(false);
const githubAccount = accounts?.find(
(account) => account.provider === "github"
);
const puterAccount = accounts?.find(
(account) => account.provider === "gitea"
);
const linkGithub = async () => {
if (pendingProvider) {
return;
}
setPendingProvider("github");
try {
await authClient.linkSocial({
callbackURL: `${window.location.origin}/projects?resume=github`,
provider: "github",
});
} finally {
setPendingProvider(undefined);
}
};
const handlePuterChipClick = () => {
if (puterAccount && isHealthy(puterAccount.status)) {
return;
}
setShowPuterForm((value) => !value);
};
const handlePuterConnected = () => {
setShowPuterForm(false);
};
const accountsLoading = accounts === undefined;
const githubConnected =
githubAccount !== undefined && isHealthy(githubAccount.status);
return (
<div className="space-y-3">
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-1">
<ProviderChip
account={githubAccount}
icon={GitBranch}
label="GitHub"
loading={accountsLoading || pendingProvider === "github"}
onClick={linkGithub}
/>
{githubConnected ? (
<GithubConnectionSettings
connectionId={githubAccount.id}
externalUsername={githubAccount.externalUsername}
grantedScopesJson={githubAccount.grantedScopesJson}
/>
) : null}
</div>
<ProviderChip
account={puterAccount}
icon={Server}
label="Puter Git"
loading={accountsLoading || pendingProvider === "gitea"}
onClick={handlePuterChipClick}
/>
</div>
{showPuterForm ? (
<div className="rounded border border-[#d7d3c7] bg-[#fffefa] p-3 shadow-sm">
<PuterConnectForm onConnected={handlePuterConnected} />
</div>
) : null}
</div>
);
};