import { A, useNavigate } from '@solidjs/router';
import { createQuery, useQueryClient } from '@tanstack/solid-query';
import { GitFork, Info, LoaderCircle } from 'lucide-solid';
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, type Component } from 'solid-js';
import { Avatar, ErrorState, LoadingState, cardStyles } from '../../components/common';
import { resolveActor } from '../../lib/api/identity';
import { forkRepo, listRepoRecords, listUserKnots } from '../../lib/api/repos';
import { useAuth } from '../../lib/auth';
import { getErrorMessage } from '../../lib/repo-utils';
import { validateRepoName } from './new';
import { useRepoQuery } from './shared';
export const ForkRepoPage: Component = () => {
const auth = useAuth();
const navigate = useNavigate();
const queryClient = useQueryClient();
const repoQuery = useRepoQuery();
const [name, setName] = createSignal('');
const [description, setDescription] = createSignal('');
const [selectedKnot, setSelectedKnot] = createSignal('');
const [saving, setSaving] = createSignal(false);
const [error, setError] = createSignal<string | null>(null);
const [initializedForRepo, setInitializedForRepo] = createSignal<string | null>(null);
const viewerQuery = createQuery(() => ({
queryKey: ['viewer', auth.currentDid()],
enabled: Boolean(auth.currentDid()),
queryFn: async () => resolveActor(auth.currentDid()!),
}));
const knotsQuery = createQuery(() => ({
queryKey: ['knots', auth.currentDid()],
enabled: Boolean(auth.currentDid()),
queryFn: async () => listUserKnots(auth.currentDid()!),
}));
createEffect(() => {
const repo = repoQuery.data;
if (!repo || initializedForRepo() === repo.repoDid) {
return;
}
setName(repo.slug);
setDescription(repo.record.value.description ?? '');
setInitializedForRepo(repo.repoDid);
});
createEffect(() => {
const knots = knotsQuery.data;
if (knots && knots.length === 1) {
setSelectedKnot(knots[0]);
}
});
const descriptionLength = createMemo(() => description().length);
const viewerLabel = createMemo(() => viewerQuery.data?.handle || auth.currentDid() || '');
const handleSubmit = async (event: SubmitEvent) => {
event.preventDefault();
const agent = auth.agent();
const currentDid = auth.currentDid();
const sourceRepo = repoQuery.data;
if (!agent || !currentDid || !sourceRepo) return;
const repoName = name().trim();
const nameError = validateRepoName(repoName);
if (nameError) {
setError(nameError);
return;
}
if (description().length > 140) {
setError('description must be 140 characters or fewer.');
return;
}
const knot = selectedKnot();
if (!knot) {
setError('please select a knot.');
return;
}
setSaving(true);
setError(null);
try {
const existingRecords = await queryClient.fetchQuery({
queryKey: ['profile-repos', currentDid],
queryFn: () => listRepoRecords(currentDid),
});
const cleanedName = repoName.endsWith('.git') ? repoName.slice(0, -4) : repoName;
const rkey = cleanedName.toLowerCase();
const exists = existingRecords.some(
(record) => record.rkey.toLowerCase() === rkey || record.value.name?.trim().toLowerCase() === rkey,
);
if (exists) {
setError(`you already have a repository named "${cleanedName}".`);
setSaving(false);
return;
}
const result = await forkRepo(agent, {
sourceRepo,
name: repoName,
description: description(),
knot,
});
await Promise.all([
queryClient.invalidateQueries({ queryKey: ['profile-repos', currentDid] }),
queryClient.invalidateQueries({ queryKey: ['repo-fork-count', sourceRepo.repoDid] }),
]);
navigate(`/${viewerLabel() || currentDid}/${result.rkey}`);
} catch (cause) {
setError(getErrorMessage(cause));
} finally {
setSaving(false);
}
};
return (
<Switch>
<Match when={repoQuery.isLoading}>
<div class={cardStyles('p-4')}>
<LoadingState label="loading repository..." />
</div>
</Match>
<Match when={repoQuery.error}>
<div class={cardStyles('p-4')}>
<ErrorState message={getErrorMessage(repoQuery.error)} />
</div>
</Match>
<Match when={repoQuery.data}>
{(repo) => (
<Show
when={auth.currentDid()}
fallback={
<div class="bg-amber-50 dark:bg-amber-900 border border-amber-500 rounded p-4 relative flex gap-2 items-center dark:text-white">
please log in to fork this repository.
</div>
}
>
<div class="grid grid-cols-12 dark:text-white">
<div class="col-span-full md:col-start-3 md:col-span-8 px-4 py-1 mb-2">
<h1 class="text-xl font-bold dark:text-white mb-1 flex items-center gap-2 flex-wrap">
<GitFork class="size-5 shrink-0" />
<span>fork</span>
<span class="flex items-center gap-1 font-normal">
<Avatar did={repo().owner.did} size="size-5" />
<A href={`/${repo().owner.handle}`} class="font-medium">
{repo().owner.handle}
</A>
<span class="text-gray-400 dark:text-gray-500">/</span>
<A href={`/${repo().owner.handle}/${repo().slug}`} class="font-semibold">
{repo().slug}
</A>
</span>
</h1>
</div>
<div class="col-span-full md:col-start-3 md:col-span-8 bg-white dark:bg-gray-800 drop-shadow-sm rounded overflow-hidden border border-gray-200 dark:border-gray-700">
<div class="flex items-center gap-3 p-3 md:px-5 border-b border-gray-100 dark:border-gray-700 text-gray-700 dark:text-gray-300">
<Info class="size-5 text-gray-500 dark:text-gray-400 shrink-0" />
<p class="text-sm">
a fork is your own copy of this repository.
<br />
use it to work independently, then open a pull request back upstream.
</p>
</div>
<form onSubmit={handleSubmit} class="p-4 md:px-6">
{error() && <p class="text-red-500 dark:text-red-400 text-sm mb-3">{error()}</p>}
<div class="untangled-timeline-step untangled-timeline-step-1 untangled-fork-step">
<div class="untangled-timeline-circle">
<div class="untangled-timeline-circle-inner">1</div>
</div>
<div class="flex-1 pb-6 pt-1">
<h2 class="untangled-fork-step-title text-lg font-medium dark:text-white">details</h2>
<div class="text-sm text-gray-500 dark:text-gray-400 mb-3">basic repository information.</div>
<div class="space-y-3 mt-3">
<div>
<label for="fork-name" class="block text-sm font-bold dark:text-white mb-1">
REPOSITORY NAME
</label>
<div class="flex flex-col md:flex-row md:items-center w-full">
<div class="shrink-0 hidden md:flex items-center px-3 py-2 gap-1.5 text-sm text-gray-700 dark:text-gray-300 border border-r-0 border-gray-300 dark:border-gray-600 rounded-l bg-gray-50 dark:bg-gray-700 h-[38px]">
<Avatar did={auth.currentDid()!} size="size-5" />
<span>{viewerLabel()}</span>
</div>
<input
type="text"
id="fork-name"
name="repo_name"
value={name()}
onInput={(event) => setName(event.currentTarget.value)}
required
class="flex-1 md:rounded-r md:rounded-l-none rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 px-3 py-2 text-sm text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 h-[38px]"
placeholder="repository-name"
/>
</div>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
choose a unique, descriptive name for your repository. use letters, numbers, and hyphens.
</p>
</div>
<div class="pt-2">
<label for="fork-description" class="block text-sm font-bold dark:text-white mb-1">
DESCRIPTION
</label>
<input
type="text"
id="fork-description"
name="description"
value={description()}
onInput={(event) => setDescription(event.currentTarget.value)}
maxlength="140"
class="w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 px-3 py-2 text-sm text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 h-[38px]"
placeholder="a brief description of your project..."
/>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
optional. a short description to help others understand what your project does ({descriptionLength()}/140 characters).
</p>
</div>
</div>
</div>
</div>
<div class="untangled-timeline-step untangled-timeline-step-2 untangled-fork-step">
<div class="untangled-timeline-circle">
<div class="untangled-timeline-circle-inner">2</div>
</div>
<div class="flex-1 pt-1">
<h2 class="untangled-fork-step-title text-lg font-medium dark:text-white">knot selection</h2>
<div class="text-sm text-gray-500 dark:text-gray-400 mb-3">repository settings and hosting.</div>
<div class="space-y-3 mt-3">
<div class="pt-2">
<label class="block text-sm font-bold dark:text-white mb-1">
SELECT A KNOT
</label>
<div class="w-full space-y-1 py-0">
<Show
when={!knotsQuery.isLoading}
fallback={<div class="text-sm text-gray-500 dark:text-gray-400">loading knots...</div>}
>
<Show
when={knotsQuery.data && knotsQuery.data.length > 0}
fallback={<p class="text-sm text-gray-500 dark:text-gray-400">no knots available.</p>}
>
<For each={knotsQuery.data}>
{(knot) => (
<div class="flex items-center leading-tight">
<input
type="radio"
name="knot"
value={knot}
checked={selectedKnot() === knot}
onChange={() => setSelectedKnot(knot)}
class="mr-2 cursor-pointer"
id={`fork-knot-${knot}`}
required
/>
<label for={`fork-knot-${knot}`} class="dark:text-white lowercase cursor-pointer">
{knot}
</label>
</div>
)}
</For>
</Show>
</Show>
</div>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
a knot hosts repository data and handles git operations. you can also{' '}
<A href="/settings/knots" class="underline">
register your own knot
</A>
.
</p>
</div>
</div>
</div>
</div>
<div class="untangled-repo-form-submit flex justify-end">
<button
type="submit"
disabled={saving() || !selectedKnot()}
class="btn-create flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
<Show when={saving()} fallback={<GitFork class="w-4 h-4" />}>
<LoaderCircle class="w-4 h-4 animate-spin" />
</Show>
fork repo
</button>
</div>
</form>
</div>
</div>
</Show>
)}
</Match>
</Switch>
);
};