diff --git a/apps/dashboard/src/components/repo/code-explorer-toolbar.tsx b/apps/dashboard/src/components/repo/code-explorer-toolbar.tsx index ce948f7..16f5fc4 100644 --- a/apps/dashboard/src/components/repo/code-explorer-toolbar.tsx +++ b/apps/dashboard/src/components/repo/code-explorer-toolbar.tsx @@ -33,6 +33,7 @@ import { githubRepoBranchesQueryOptions, } from "#/lib/github.query"; import type { RepoOverview } from "#/lib/github.types"; +import { useRepoCloneProtocol } from "#/lib/repo-clone-protocol-storage"; export function CodeExplorerToolbar({ repo, @@ -177,12 +178,20 @@ function BranchSelector({ } function CodePopover({ repo }: { repo: RepoOverview }) { + const [selectedProtocol, setSelectedProtocol] = useRepoCloneProtocol(); const [copied, setCopied] = useState(false); const httpsUrl = `https://github.com/${repo.fullName}.git`; const sshUrl = `git@github.com:${repo.fullName}.git`; const cliCommand = `gh repo clone ${repo.fullName}`; const zipUrl = `https://github.com/${repo.fullName}/archive/refs/heads/${repo.defaultBranch}.zip`; + const handleProtocolChange = useCallback( + (protocol: string) => { + setSelectedProtocol(protocol); + }, + [setSelectedProtocol], + ); + const handleCopy = useCallback((text: string) => { void navigator.clipboard.writeText(text); setCopied(true); @@ -203,7 +212,11 @@ function CodePopover({ repo }: { repo: RepoOverview }) { Clone {repo.fullName} - + HTTPS diff --git a/apps/dashboard/src/lib/repo-clone-protocol-storage.ts b/apps/dashboard/src/lib/repo-clone-protocol-storage.ts new file mode 100644 index 0000000..2eb5476 --- /dev/null +++ b/apps/dashboard/src/lib/repo-clone-protocol-storage.ts @@ -0,0 +1,40 @@ +import { useCallback } from "react"; +import { useLocalStorageState } from "./use-local-storage-state"; + +export type CloneProtocol = "https" | "ssh" | "cli"; + +const CLONE_PROTOCOL_STORAGE_KEY = "diffkit:repo-clone-protocol"; +const DEFAULT_CLONE_PROTOCOL: CloneProtocol = "cli"; + +const VALID_CLONE_PROTOCOLS = { + https: true, + ssh: true, + cli: true, +} satisfies Record; + +export function isCloneProtocol(value: unknown): value is CloneProtocol { + return typeof value === "string" && value in VALID_CLONE_PROTOCOLS; +} + +export function useRepoCloneProtocol() { + const [cloneProtocol, setCloneProtocol] = useLocalStorageState( + CLONE_PROTOCOL_STORAGE_KEY, + { + defaultValue: DEFAULT_CLONE_PROTOCOL, + validate: isCloneProtocol, + }, + ); + + const setStoredCloneProtocol = useCallback( + (protocol: string) => { + if (!isCloneProtocol(protocol)) { + return; + } + + setCloneProtocol(protocol); + }, + [setCloneProtocol], + ); + + return [cloneProtocol, setStoredCloneProtocol] as const; +} diff --git a/apps/dashboard/src/lib/use-local-storage-state.ts b/apps/dashboard/src/lib/use-local-storage-state.ts new file mode 100644 index 0000000..236a275 --- /dev/null +++ b/apps/dashboard/src/lib/use-local-storage-state.ts @@ -0,0 +1,58 @@ +import { useCallback, useState } from "react"; + +type LocalStorageStateOptions = { + defaultValue: T; + validate: (value: unknown) => value is T; + parse?: (raw: string) => unknown; + serialize?: (value: T) => string; +}; + +export function useLocalStorageState( + key: string, + { + defaultValue, + validate, + parse = (raw) => raw, + serialize = String, + }: LocalStorageStateOptions, +) { + const [value, setValue] = useState(() => { + if (typeof window === "undefined") { + return defaultValue; + } + + try { + const stored = window.localStorage.getItem(key); + if (stored === null) { + return defaultValue; + } + + const parsed = parse(stored); + return validate(parsed) ? parsed : defaultValue; + } catch { + return defaultValue; + } + }); + + const setStoredValue = useCallback( + (nextValue: T | ((currentValue: T) => T)) => { + setValue((currentValue) => { + const resolvedValue = + typeof nextValue === "function" + ? (nextValue as (currentValue: T) => T)(currentValue) + : nextValue; + + try { + window.localStorage.setItem(key, serialize(resolvedValue)); + } catch { + // ignore + } + + return resolvedValue; + }); + }, + [key, serialize], + ); + + return [value, setStoredValue] as const; +}