refactor projects page

This commit is contained in:
-Puter
2026-07-31 18:53:04 +05:30
parent ce1f33aa69
commit fc8cfabacb
12 changed files with 508 additions and 476 deletions

View File

@@ -0,0 +1,69 @@
import { api } from "@code/backend/convex/_generated/api";
import type { Id } from "@code/backend/convex/_generated/dataModel";
import { Button } from "@code/ui/components/button";
import { useMutation, useQuery } from "convex/react";
import { LoaderCircle } from "lucide-react";
import { useState } from "react";
export const ContextEditor = ({
projectId,
}: {
readonly projectId: Id<"projects">;
}) => {
const instructions = useQuery(api.projects.getInstructions, {
projectId,
});
const updateInstructions = useMutation(api.projects.updateInstructions);
const [draft, setDraft] = useState<string>();
const [saving, setSaving] = useState(false);
const currentDraft = draft ?? instructions ?? "";
const dirty = draft !== undefined && draft !== (instructions ?? "");
const save = async () => {
if (!dirty || saving) {
return;
}
setSaving(true);
try {
await updateInstructions({
instructions: draft ?? "",
projectId,
});
setDraft(undefined);
} finally {
setSaving(false);
}
};
return (
<div className="mt-3">
<label
className="text-xs font-medium text-[#20201d]"
htmlFor="context-textarea"
>
Context
</label>
<textarea
className="mt-1 h-24 w-full resize-y border border-[#c9c5b9] bg-[#fffefa] p-2 text-xs outline-none focus:border-[#55564e]"
id="context-textarea"
onChange={(event) => setDraft(event.target.value)}
placeholder="Project-specific instructions for agents..."
value={currentDraft}
/>
{dirty ? (
<Button
className="mt-1 h-8 text-xs"
disabled={saving}
onClick={save}
size="sm"
type="button"
variant="outline"
>
{saving ? <LoaderCircle className="size-3 animate-spin" /> : null}
{saving ? "Saving" : "Save context"}
</Button>
) : null}
</div>
);
};

View File

@@ -0,0 +1,15 @@
import { FolderGit2 } from "lucide-react";
export const EmptyProjects = () => (
<div className="mt-12 text-center">
<span className="grid size-11 place-items-center bg-[#20201d] text-white">
<FolderGit2 className="size-5" />
</span>
<h1 className="mt-6 text-2xl font-semibold text-[#20201d]">
Connect your first project
</h1>
<p className="mt-2 text-sm text-[#68665e]">
Choose a Git provider below to get started.
</p>
</div>
);

View File

@@ -0,0 +1,36 @@
import { authClient } from "@code/auth/web";
import { LoaderCircle, GitBranch } from "lucide-react";
import { useState } from "react";
export const GitHubConnectButton = () => {
const [pending, setPending] = useState(false);
const linkGithub = async () => {
setPending(true);
try {
await authClient.linkSocial({
callbackURL: `${window.location.origin}/projects?resume=github`,
provider: "github",
});
} finally {
setPending(false);
}
};
return (
<button
className="flex w-full items-center gap-3 border border-[#d7d3c7] bg-[#fffefa] p-4 transition-colors hover:bg-[#f5f3eb]"
onClick={linkGithub}
type="button"
>
<GitBranch className="size-5 text-[#20201d]" />
<div className="flex-1 text-left">
<p className="text-sm font-semibold text-[#20201d]">GitHub</p>
<p className="text-xs text-[#747168]">OAuth with private repo access</p>
</div>
{pending ? (
<LoaderCircle className="size-4 animate-spin text-[#747168]" />
) : null}
</button>
);
};

View File

@@ -0,0 +1,7 @@
import { LoaderCircle } from "lucide-react";
export const LoadingState = () => (
<main className="grid min-h-svh place-items-center bg-[#f2f0e7]">
<LoaderCircle className="size-5 animate-spin text-[#20201d]" />
</main>
);

View File

@@ -0,0 +1,31 @@
import { FileText, FolderGit2 } from "lucide-react";
import { Link } from "react-router";
export const ProjectCard = ({
instructions,
name,
projectId,
sourceUrl,
}: {
readonly instructions: string;
readonly name: string;
readonly projectId: string;
readonly sourceUrl: string;
}) => (
<Link
className="block border border-[#d7d3c7] bg-[#fffefa] p-4 transition-colors hover:bg-[#f5f3eb]"
to={`/?project=${projectId}`}
>
<div className="flex items-center gap-2">
<FolderGit2 className="size-4 text-[#747168]" />
<h3 className="text-sm font-semibold text-[#20201d]">{name}</h3>
</div>
<p className="mt-1 truncate text-xs text-[#747168]">{sourceUrl}</p>
{instructions ? (
<div className="mt-2 flex items-center gap-1 text-xs text-[#747168]">
<FileText className="size-3" />
<span className="truncate">Context configured</span>
</div>
) : null}
</Link>
);

View File

@@ -1,465 +0,0 @@
import { authClient } from "@code/auth/web";
import { api } from "@code/backend/convex/_generated/api";
import type { Id } from "@code/backend/convex/_generated/dataModel";
import { Button } from "@code/ui/components/button";
import { useAction, useMutation, useQuery } from "convex/react";
import { makeFunctionReference } from "convex/server";
import {
FileText,
FolderGit2,
GitBranch,
LoaderCircle,
Server,
} from "lucide-react";
import { useEffect, useState } from "react";
import { Link, useNavigate, useSearchParams } from "react-router";
const connectGithubRef = makeFunctionReference<
"action",
Record<string, never>,
{ connectionId: Id<"gitConnections"> }
>("gitConnections:connectGithub");
const connectGiteaRef = makeFunctionReference<
"action",
{ token: string; username?: string },
{ connectionId: Id<"gitConnections"> }
>("gitConnections:connectGitea");
const createProjectFromRepositoryRef = makeFunctionReference<
"mutation",
{ gitRepositoryId: Id<"gitRepositories">; name: string },
{ projectId: Id<"projects"> }
>("gitProvisioning:createProjectFromRepository");
const LoadingState = () => (
<main className="grid min-h-svh place-items-center bg-[#f2f0e7]">
<LoaderCircle className="size-5 animate-spin text-[#20201d]" />
</main>
);
const EmptyProjects = () => (
<div className="mt-12 text-center">
<span className="grid size-11 place-items-center bg-[#20201d] text-white">
<FolderGit2 className="size-5" />
</span>
<h1 className="mt-6 text-2xl font-semibold text-[#20201d]">
Connect your first project
</h1>
<p className="mt-2 text-sm text-[#68665e]">
Choose a Git provider below to get started.
</p>
</div>
);
const ProjectCard = ({
instructions,
name,
projectId,
sourceUrl,
}: {
instructions: string;
name: string;
projectId: string;
sourceUrl: string;
}) => (
<Link
className="block border border-[#d7d3c7] bg-[#fffefa] p-4 transition-colors hover:bg-[#f5f3eb]"
to={`/?project=${projectId}`}
>
<div className="flex items-center gap-2">
<FolderGit2 className="size-4 text-[#747168]" />
<h3 className="text-sm font-semibold text-[#20201d]">{name}</h3>
</div>
<p className="mt-1 truncate text-xs text-[#747168]">{sourceUrl}</p>
{instructions ? (
<div className="mt-2 flex items-center gap-1 text-xs text-[#747168]">
<FileText className="size-3" />
<span className="truncate">Context configured</span>
</div>
) : null}
</Link>
);
const ContextEditor = ({
projectId,
}: {
readonly projectId: Id<"projects">;
}) => {
const instructions = useQuery(api.projects.getInstructions, {
projectId,
});
const updateInstructions = useMutation(api.projects.updateInstructions);
const [draft, setDraft] = useState<string>();
const [saving, setSaving] = useState(false);
const currentDraft = draft ?? instructions ?? "";
const dirty = draft !== undefined && draft !== (instructions ?? "");
const save = async () => {
if (!dirty || saving) {
return;
}
setSaving(true);
try {
await updateInstructions({
instructions: draft ?? "",
projectId,
});
setDraft(undefined);
} finally {
setSaving(false);
}
};
return (
<div className="mt-3">
<label
className="text-xs font-medium text-[#20201d]"
htmlFor="context-textarea"
>
Context
</label>
<textarea
className="mt-1 h-24 w-full resize-y border border-[#c9c5b9] bg-[#fffefa] p-2 text-xs outline-none focus:border-[#55564e]"
id="context-textarea"
onChange={(event) => setDraft(event.target.value)}
placeholder="Project-specific instructions for agents..."
value={currentDraft}
/>
{dirty ? (
<Button
className="mt-1 h-8 text-xs"
disabled={saving}
onClick={save}
size="sm"
type="button"
variant="outline"
>
{saving ? <LoaderCircle className="size-3 animate-spin" /> : null}
{saving ? "Saving" : "Save context"}
</Button>
) : null}
</div>
);
};
const PuterConnectForm = ({
onConnected,
}: {
readonly onConnected: () => void;
}) => {
const connectGitea = useAction(connectGiteaRef);
const [token, setToken] = useState("");
const [username, setUsername] = useState("");
const [pending, setPending] = useState(false);
const [error, setError] = useState<string>();
const submit = async (event: React.FormEvent) => {
event.preventDefault();
if (!token.trim() || pending) {
return;
}
setPending(true);
setError(undefined);
try {
await connectGitea({
token: token.trim(),
username: username.trim() || undefined,
});
setToken("");
setUsername("");
onConnected();
} catch (caughtError) {
setError(
caughtError instanceof Error ? caughtError.message : String(caughtError)
);
} finally {
setPending(false);
}
};
return (
<form className="mt-4 space-y-3" onSubmit={submit}>
<p className="text-xs text-[#68665e]">
Connect your Puter Git personal access token. The Puter Git instance is
at <code className="text-[#20201d]">git.openputer.com</code>.
</p>
<input
className="h-10 w-full border border-[#c9c5b9] bg-[#fffefa] px-3 text-sm outline-none focus:border-[#55564e]"
onChange={(event) => setUsername(event.target.value)}
placeholder="Puter username (optional)"
value={username}
/>
<input
className="h-10 w-full border border-[#c9c5b9] bg-[#fffefa] px-3 text-sm outline-none focus:border-[#55564e]"
onChange={(event) => setToken(event.target.value)}
placeholder="Personal access token"
required
type="password"
value={token}
/>
{error ? <p className="text-xs text-red-700">{error}</p> : null}
<Button className="h-10 w-full" disabled={pending} type="submit">
{pending ? <LoaderCircle className="size-4 animate-spin" /> : null}
{pending ? "Connecting" : "Connect Puter Git"}
</Button>
</form>
);
};
const RepositorySelector = ({
onProjectCreated,
}: {
readonly onProjectCreated: (projectId: string) => void;
}) => {
const repositories = useQuery(
makeFunctionReference<
"query",
Record<string, never>,
readonly {
cloneUrl: string;
defaultBranch: string;
fullName: string;
id: string;
name: string;
owner: string;
privacy: "public" | "private";
provider: "github" | "gitea";
webUrl: string;
}[]
>("gitProvisioning:listRepositories"),
{}
);
const createProject = useMutation(createProjectFromRepositoryRef);
const [pending, setPending] = useState<string>();
const create = async (repoId: string, name: string) => {
setPending(repoId);
try {
const result = await createProject({
gitRepositoryId: repoId as Id<"gitRepositories">,
name,
});
onProjectCreated(String(result.projectId));
} finally {
setPending(undefined);
}
};
if (repositories === undefined) {
return <LoaderCircle className="size-4 animate-spin text-[#747168]" />;
}
if (repositories.length === 0) {
return (
<p className="text-xs text-[#747168]">
No repositories found. Create one on your provider first, or import a
public repository below.
</p>
);
}
return (
<div className="mt-3 space-y-2">
{repositories.map((repo) => (
<button
className="flex w-full items-center justify-between border border-[#d7d3c7] bg-[#fffefa] p-3 text-left transition-colors hover:bg-[#f5f3eb]"
key={repo.id}
onClick={() => create(repo.id, repo.name)}
type="button"
>
<div>
<p className="text-sm font-medium text-[#20201d]">
{repo.fullName}
</p>
<p className="text-xs text-[#747168]">
{repo.provider} - {repo.defaultBranch}
</p>
</div>
{pending === repo.id ? (
<LoaderCircle className="size-4 animate-spin text-[#747168]" />
) : null}
</button>
))}
</div>
);
};
const GitHubConnectButton = () => {
const [pending, setPending] = useState(false);
const linkGithub = async () => {
setPending(true);
try {
await authClient.linkSocial({
callbackURL: `${window.location.origin}/projects?resume=github`,
provider: "github",
});
} finally {
setPending(false);
}
};
return (
<button
className="flex w-full items-center gap-3 border border-[#d7d3c7] bg-[#fffefa] p-4 transition-colors hover:bg-[#f5f3eb]"
onClick={linkGithub}
type="button"
>
<GitBranch className="size-5 text-[#20201d]" />
<div className="flex-1 text-left">
<p className="text-sm font-semibold text-[#20201d]">GitHub</p>
<p className="text-xs text-[#747168]">OAuth with private repo access</p>
</div>
{pending ? (
<LoaderCircle className="size-4 animate-spin text-[#747168]" />
) : null}
</button>
);
};
const ProviderSection = () => {
const [showPuterForm, setShowPuterForm] = useState(false);
return (
<section className="mt-10">
<h2 className="text-sm font-semibold text-[#20201d]">Git providers</h2>
<p className="mt-1 text-xs text-[#68665e]">
Connect a repository to create a Project.
</p>
<div className="mt-4 space-y-3">
<GitHubConnectButton />
<button
className="flex w-full items-center gap-3 border border-[#d7d3c7] bg-[#fffefa] p-4 transition-colors hover:bg-[#f5f3eb]"
onClick={() => setShowPuterForm((v) => !v)}
type="button"
>
<Server className="size-5 text-[#20201d]" />
<div className="flex-1 text-left">
<p className="text-sm font-semibold text-[#20201d]">Puter Git</p>
<p className="text-xs text-[#747168]">
Connect with a personal access token
</p>
</div>
</button>
</div>
{showPuterForm ? (
<PuterConnectForm onConnected={() => setShowPuterForm(false)} />
) : null}
</section>
);
};
const RepositorySection = ({
onProjectCreated,
}: {
readonly onProjectCreated: (projectId: string) => void;
}) => (
<section className="mt-6">
<h2 className="text-sm font-semibold text-[#20201d]">Your repositories</h2>
<p className="mt-1 text-xs text-[#68665e]">
Select a repository to create a Project.
</p>
<RepositorySelector onProjectCreated={onProjectCreated} />
</section>
);
export const ProjectsPage = () => {
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const projects = useQuery(api.projects.list, {});
const connectGithubAction = useAction(connectGithubRef);
const [resumeError, setResumeError] = useState<string>();
// Handle GitHub OAuth callback resume
useEffect(() => {
const resume = searchParams.get("resume");
if (resume !== "github") {
return;
}
void (async () => {
try {
await connectGithubAction({});
setSearchParams({}, { replace: true });
} catch (caughtError) {
setResumeError(
caughtError instanceof Error
? caughtError.message
: "GitHub connection failed"
);
}
})();
}, [searchParams, setSearchParams, connectGithubAction]);
if (projects === undefined) {
return <LoadingState />;
}
const hasProjects = projects.length > 0;
const handleProjectCreated = (projectId: string) => {
void navigate(`/?project=${projectId}`, { replace: true });
};
return (
<main className="min-h-svh bg-[#f2f0e7] px-5 py-8 text-[#20201d]">
<div className="mx-auto max-w-2xl">
<header className="flex items-center justify-between">
<h1 className="text-lg font-semibold">Projects</h1>
{hasProjects ? (
<Button
className="h-9"
onClick={() => navigate("/")}
size="sm"
variant="outline"
>
Home
</Button>
) : null}
</header>
{resumeError ? (
<p className="mt-4 border border-red-300 bg-red-50 p-3 text-xs text-red-700">
{resumeError}
</p>
) : null}
{hasProjects ? null : <EmptyProjects />}
{hasProjects ? (
<section className="mt-6 grid gap-3">
{projects.map((project) => (
<div key={project.id}>
<ProjectCard
instructions={
project.contextDocuments.find(
(doc) => doc.kind === "readme"
)?.content ?? ""
}
name={project.name}
projectId={project.id}
sourceUrl={project.sources[0]?.url ?? ""}
/>
<ContextEditor
projectId={project.id as unknown as Id<"projects">}
/>
</div>
))}
</section>
) : null}
<RepositorySection onProjectCreated={handleProjectCreated} />
<ProviderSection />
<section className="mt-8">
<p className="text-xs text-[#68665e]">
Or import a public repository:
</p>
<Link
className="mt-2 inline-flex items-center gap-2 text-sm text-[#20201d] underline"
to="/?import=public"
>
Import public Git URL
</Link>
</section>
</div>
</main>
);
};

View File

@@ -0,0 +1,37 @@
import { Server } from "lucide-react";
import { useState } from "react";
import { GitHubConnectButton } from "./github-connect-button";
import { PuterConnectForm } from "./puter-connect-form";
export const ProviderSection = () => {
const [showPuterForm, setShowPuterForm] = useState(false);
return (
<section className="mt-10">
<h2 className="text-sm font-semibold text-[#20201d]">Git providers</h2>
<p className="mt-1 text-xs text-[#68665e]">
Connect a repository to create a Project.
</p>
<div className="mt-4 space-y-3">
<GitHubConnectButton />
<button
className="flex w-full items-center gap-3 border border-[#d7d3c7] bg-[#fffefa] p-4 transition-colors hover:bg-[#f5f3eb]"
onClick={() => setShowPuterForm((value) => !value)}
type="button"
>
<Server className="size-5 text-[#20201d]" />
<div className="flex-1 text-left">
<p className="text-sm font-semibold text-[#20201d]">Puter Git</p>
<p className="text-xs text-[#747168]">
Connect with a personal access token
</p>
</div>
</button>
</div>
{showPuterForm ? (
<PuterConnectForm onConnected={() => setShowPuterForm(false)} />
) : null}
</section>
);
};

View File

@@ -0,0 +1,80 @@
import type { Id } from "@code/backend/convex/_generated/dataModel";
import { Button } from "@code/ui/components/button";
import { useAction } from "convex/react";
import { makeFunctionReference } from "convex/server";
import { LoaderCircle } from "lucide-react";
import { useState } from "react";
interface ConnectGiteaResult {
connectionId: Id<"gitConnections">;
}
const connectGiteaRef = makeFunctionReference<
"action",
{ token: string; username?: string },
ConnectGiteaResult
>("gitConnections:connectGitea");
export const PuterConnectForm = ({
onConnected,
}: {
readonly onConnected: () => void;
}) => {
const connectGitea = useAction(connectGiteaRef);
const [token, setToken] = useState("");
const [username, setUsername] = useState("");
const [pending, setPending] = useState(false);
const [error, setError] = useState<string>();
const submit = async (event: React.FormEvent) => {
event.preventDefault();
if (!token.trim() || pending) {
return;
}
setPending(true);
setError(undefined);
try {
await connectGitea({
token: token.trim(),
username: username.trim() || undefined,
});
setToken("");
setUsername("");
onConnected();
} catch (caughtError) {
setError(
caughtError instanceof Error ? caughtError.message : String(caughtError)
);
} finally {
setPending(false);
}
};
return (
<form className="mt-4 space-y-3" onSubmit={submit}>
<p className="text-xs text-[#68665e]">
Connect your Puter Git personal access token. The Puter Git instance is
at <code className="text-[#20201d]">git.openputer.com</code>.
</p>
<input
className="h-10 w-full border border-[#c9c5b9] bg-[#fffefa] px-3 text-sm outline-none focus:border-[#55564e]"
onChange={(event) => setUsername(event.target.value)}
placeholder="Puter username (optional)"
value={username}
/>
<input
className="h-10 w-full border border-[#c9c5b9] bg-[#fffefa] px-3 text-sm outline-none focus:border-[#55564e]"
onChange={(event) => setToken(event.target.value)}
placeholder="Personal access token"
required
type="password"
value={token}
/>
{error ? <p className="text-xs text-red-700">{error}</p> : null}
<Button className="h-10 w-full" disabled={pending} type="submit">
{pending ? <LoaderCircle className="size-4 animate-spin" /> : null}
{pending ? "Connecting" : "Connect Puter Git"}
</Button>
</form>
);
};

View File

@@ -0,0 +1,15 @@
import { RepositorySelector } from "./repository-selector";
export const RepositorySection = ({
onProjectCreated,
}: {
readonly onProjectCreated: (projectId: string) => void;
}) => (
<section className="mt-6">
<h2 className="text-sm font-semibold text-[#20201d]">Your repositories</h2>
<p className="mt-1 text-xs text-[#68665e]">
Select a repository to create a Project.
</p>
<RepositorySelector onProjectCreated={onProjectCreated} />
</section>
);

View File

@@ -0,0 +1,91 @@
import type { Id } from "@code/backend/convex/_generated/dataModel";
import { useMutation, useQuery } from "convex/react";
import { makeFunctionReference } from "convex/server";
import { LoaderCircle } from "lucide-react";
import { useState } from "react";
const listRepositoriesRef = makeFunctionReference<
"query",
Record<string, never>,
readonly {
cloneUrl: string;
defaultBranch: string;
fullName: string;
id: string;
name: string;
owner: string;
privacy: "public" | "private";
provider: "github" | "gitea";
webUrl: string;
}[]
>("gitProvisioning:listRepositories");
interface CreateProjectResult {
projectId: Id<"projects">;
}
const createProjectFromRepositoryRef = makeFunctionReference<
"mutation",
{ gitRepositoryId: Id<"gitRepositories">; name: string },
CreateProjectResult
>("gitProvisioning:createProjectFromRepository");
export const RepositorySelector = ({
onProjectCreated,
}: {
readonly onProjectCreated: (projectId: string) => void;
}) => {
const repositories = useQuery(listRepositoriesRef, {});
const createProject = useMutation(createProjectFromRepositoryRef);
const [pending, setPending] = useState<string>();
const create = async (repoId: string, name: string) => {
setPending(repoId);
try {
const result = await createProject({
gitRepositoryId: repoId as Id<"gitRepositories">,
name,
});
onProjectCreated(String(result.projectId));
} finally {
setPending(undefined);
}
};
if (repositories === undefined) {
return <LoaderCircle className="size-4 animate-spin text-[#747168]" />;
}
if (repositories.length === 0) {
return (
<p className="text-xs text-[#747168]">
No repositories found. Create one on your provider first, or import a
public repository below.
</p>
);
}
return (
<div className="mt-3 space-y-2">
{repositories.map((repo) => (
<button
className="flex w-full items-center justify-between border border-[#d7d3c7] bg-[#fffefa] p-3 text-left transition-colors hover:bg-[#f5f3eb]"
key={repo.id}
onClick={() => create(repo.id, repo.name)}
type="button"
>
<div>
<p className="text-sm font-medium text-[#20201d]">
{repo.fullName}
</p>
<p className="text-xs text-[#747168]">
{repo.provider} - {repo.defaultBranch}
</p>
</div>
{pending === repo.id ? (
<LoaderCircle className="size-4 animate-spin text-[#747168]" />
) : null}
</button>
))}
</div>
);
};

View File

@@ -1,5 +1,121 @@
import { ProjectsPage } from "@/components/projects/projects-page";
import { api } from "@code/backend/convex/_generated/api";
import type { Id } from "@code/backend/convex/_generated/dataModel";
import { Button } from "@code/ui/components/button";
import { useAction, useQuery } from "convex/react";
import { makeFunctionReference } from "convex/server";
import { useEffect, useState } from "react";
import { Link, useNavigate, useSearchParams } from "react-router";
import { ContextEditor } from "@/components/projects/context-editor";
import { EmptyProjects } from "@/components/projects/empty-projects";
import { LoadingState } from "@/components/projects/loading-state";
import { ProjectCard } from "@/components/projects/project-card";
import { ProviderSection } from "@/components/projects/provider-section";
import { RepositorySection } from "@/components/projects/repository-section";
const connectGithubRef = makeFunctionReference<
"action",
Record<string, never>,
{ connectionId: Id<"gitConnections"> }
>("gitConnections:connectGithub");
export default function ProjectsRoute() {
return <ProjectsPage />;
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const projects = useQuery(api.projects.list, {});
const connectGithubAction = useAction(connectGithubRef);
const [resumeError, setResumeError] = useState<string>();
useEffect(() => {
const resume = searchParams.get("resume");
if (resume !== "github") {
return;
}
void (async () => {
try {
await connectGithubAction({});
setSearchParams({}, { replace: true });
} catch (caughtError) {
setResumeError(
caughtError instanceof Error
? caughtError.message
: "GitHub connection failed"
);
}
})();
}, [searchParams, setSearchParams, connectGithubAction]);
if (projects === undefined) {
return <LoadingState />;
}
const hasProjects = projects.length > 0;
const handleProjectCreated = (projectId: string) => {
void navigate(`/?project=${projectId}`, { replace: true });
};
return (
<main className="min-h-svh bg-[#f2f0e7] px-5 py-8 text-[#20201d]">
<div className="mx-auto max-w-2xl">
<header className="flex items-center justify-between">
<h1 className="text-lg font-semibold">Projects</h1>
{hasProjects ? (
<Button
className="h-9"
onClick={() => navigate("/")}
size="sm"
variant="outline"
>
Home
</Button>
) : null}
</header>
{resumeError ? (
<p className="mt-4 border border-red-300 bg-red-50 p-3 text-xs text-red-700">
{resumeError}
</p>
) : null}
{hasProjects ? null : <EmptyProjects />}
{hasProjects ? (
<section className="mt-6 grid gap-3">
{projects.map((project) => (
<div key={project.id}>
<ProjectCard
instructions={
project.contextDocuments.find(
(document) => document.kind === "readme"
)?.content ?? ""
}
name={project.name}
projectId={project.id}
sourceUrl={project.sources[0]?.url ?? ""}
/>
<ContextEditor
projectId={project.id as unknown as Id<"projects">}
/>
</div>
))}
</section>
) : null}
<RepositorySection onProjectCreated={handleProjectCreated} />
<ProviderSection />
<section className="mt-8">
<p className="text-xs text-[#68665e]">
Or import a public repository:
</p>
<Link
className="mt-2 inline-flex items-center gap-2 text-sm text-[#20201d] underline"
to="/?import=public"
>
Import public Git URL
</Link>
</section>
</div>
</main>
);
}