import { useEffect, useMemo, useState } from "react"; import type { ImportJobSummary, IntegrationSummary, SessionUser } from "@ledger/shared"; import { api } from "../lib/api"; import { EmptyState } from "./EmptyState"; import { PageHeader } from "./PageHeader"; type Space = { id: string; name: string; key: string; visibility: string; }; type ImportSource = "markdown" | "github" | "google_docs"; export function ImportsPage({ user, spaces }: { user: SessionUser | null; spaces: Space[]; }) { const [source, setSource] = useState("markdown"); const [step, setStep] = useState<"configure" | "preview" | "imported">("configure"); const [integrations, setIntegrations] = useState([]); const [jobs, setJobs] = useState([]); const [status, setStatus] = useState(null); const [markdownFiles, setMarkdownFiles] = useState>([]); const [markdownPreview, setMarkdownPreview] = useState | null>(null); const [github, setGithub] = useState({ repo: "", branch: "main", path: "" }); const [githubPreview, setGithubPreview] = useState<{ title: string; excerpt?: string } | null>(null); const [googleDocId, setGoogleDocId] = useState(""); const [googlePreview, setGooglePreview] = useState<{ title: string; excerpt?: string } | null>(null); const [target, setTarget] = useState({ spaceId: spaces[0]?.id ?? "", visibility: "internal", state: "draft" }); const canImport = Boolean(user && user.role !== "viewer" && user.role !== "public"); const githubIntegration = integrations.find((item) => item.provider === "github"); const googleIntegration = integrations.find((item) => item.provider === "google_docs"); useEffect(() => { if (!canImport) return; api.get<{ integrations: IntegrationSummary[] }>("/api/integrations").then((response) => setIntegrations(response.integrations)); api.get<{ jobs: ImportJobSummary[] }>("/api/integrations/import-jobs").then((response) => setJobs(response.jobs)); }, [canImport]); const sourceCards = useMemo( () => [ { key: "markdown" as const, title: "Markdown Import", description: "Import one or more Markdown files directly from your browser.", status: "Ready" }, { key: "github" as const, title: "GitHub", description: "Import Markdown content from a repository path and branch.", status: githubIntegration?.statusMessage ?? "Not configured" }, { key: "google_docs" as const, title: "Google Docs", description: "Import a Google Doc into Markdown while preserving basic formatting.", status: googleIntegration?.statusMessage ?? "Not configured" } ], [githubIntegration?.statusMessage, googleIntegration?.statusMessage] ); function sourceBadgeTone(key: ImportSource) { if (key === "markdown") return "public"; if (key === "github") return githubIntegration?.status === "configured" ? "public" : "restricted"; return googleIntegration?.status === "configured" ? "public" : "restricted"; } async function handleMarkdownFiles(fileList: FileList | null) { if (!fileList) return; try { const files = await Promise.all( Array.from(fileList).map( (file) => new Promise<{ fileName: string; content: string }>((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve({ fileName: file.name, content: String(reader.result ?? "") }); reader.onerror = () => reject(reader.error); reader.readAsText(file); }) ) ); setMarkdownFiles(files); const response = await api.post<{ documents: Array<{ title: string; excerpt?: string }> }>("/api/integrations/markdown/preview", { files }); setMarkdownPreview(response.documents); setStep("preview"); setStatus(null); } catch (error) { setStatus(error instanceof Error ? error.message : "Could not preview Markdown files."); } } async function previewGithub() { try { const response = await api.post<{ document: { title: string; excerpt?: string } }>("/api/integrations/github/preview", github); setGithubPreview(response.document); setStep("preview"); setStatus(null); } catch (error) { setStatus(error instanceof Error ? error.message : "Could not preview GitHub content."); } } async function previewGoogle() { try { const response = await api.post<{ document: { title: string; excerpt?: string } }>("/api/integrations/google-docs/preview", { documentId: googleDocId }); setGooglePreview(response.document); setStep("preview"); setStatus(null); } catch (error) { setStatus(error instanceof Error ? error.message : "Could not preview Google Docs content."); } } async function runImport() { try { if (source === "markdown") { await api.post("/api/integrations/markdown/import", { files: markdownFiles, target }); } if (source === "github") { await api.post("/api/integrations/github/import", { ...github, target }); } if (source === "google_docs") { await api.post("/api/integrations/google-docs/import", { documentId: googleDocId, target }); } setStatus("Import completed successfully."); setStep("imported"); if (canImport) { const response = await api.get<{ jobs: ImportJobSummary[] }>("/api/integrations/import-jobs"); setJobs(response.jobs); } } catch (error) { setStatus(error instanceof Error ? error.message : "Import failed."); } } const sourceReady = source === "markdown" || (source === "github" && githubIntegration?.status === "configured") || (source === "google_docs" && googleIntegration?.status === "configured"); return (
{!canImport ? (
) : ( <>

Step 1

Select a source

{sourceCards.map((card) => ( ))}

Step 2

Configure target

Step 3

Preview source content

{!sourceReady ? ( ) : null} {source === "markdown" ? (
void handleMarkdownFiles(event.target.files)} />

Multiple-file upload is supported directly. Folder upload varies by browser, so Ledger uses multiple files as the reliable default.

{markdownPreview ? (
{markdownPreview.map((document) => (
{document.title}

{document.excerpt ?? "No excerpt available."}

))}
) : null}
) : null} {source === "github" ? (
{githubPreview ?
{githubPreview.title}

{githubPreview.excerpt ?? "No excerpt available."}

: null}
) : null} {source === "google_docs" ? (
{googlePreview ?
{googlePreview.title}

{googlePreview.excerpt ?? "No excerpt available."}

: null}
) : null}

Step 4

Import

{status ?

{status}

: null} {step === "imported" ?

Import finished. Source metadata will appear on imported pages.

: null}

History

Recent imports

{jobs.length === 0 ? ( ) : (
{jobs.map((job) => (
{job.sourceLabel} {job.status}

{job.provider} • {job.importedCount} pages

{job.errorMessage ?

{job.errorMessage}

: null}
))}
)}
)}
); }