Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/app/editor/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,13 @@ export default function EditorPage({ params }: { params: { id: string } }) {
)}
</div>
}
right={<PreviewPane projectId={id} version={pdfVersion} />}
right={
<PreviewPane
projectId={id}
version={pdfVersion}
documentStatus={status}
/>
}
/>
</div>
</div>
Expand Down
6 changes: 2 additions & 4 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import Image from "next/image";
import { listProjects } from "@/lib/projects";
import { templates } from "@/lib/templates";
import { NewProjectButton } from "@/components/dashboard/NewProjectButton";
Expand All @@ -21,9 +20,8 @@ export default function DashboardPage() {
return (
<div className="min-h-screen">
<header className="sticky top-0 z-10 glass border-b border-border">
<div className="mx-auto flex h-14 max-w-5xl items-center gap-2.5 px-6">
<Image src="/TexSet.svg" alt="" width={26} height={26} priority />
<span className="text-base font-semibold">TexSet</span>
<div className="mx-auto flex h-14 max-w-5xl items-center px-6">
<span className="text-base font-semibold tracking-tight">TexSet</span>
</div>
</header>

Expand Down
49 changes: 31 additions & 18 deletions src/components/editor/PreviewPane.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
"use client";

import { useEffect, useRef, useState } from "react";
import type { CompileStatus } from "./useCompile";

type PreviewState = "loading" | "ready" | "empty" | "error";
type RenderState = "loading" | "ready" | "blank" | "error";

interface PreviewPaneProps {
projectId: string;
// bumped after each successful compile so we know to re-render the PDF
version: number;
// the compile outcome, used to explain why there's nothing to show yet
documentStatus: CompileStatus;
}

// Renders the compiled PDF with pdf.js, one canvas per page. The worker is
// bundled from the package so it keeps working offline.
export function PreviewPane({ projectId, version }: PreviewPaneProps) {
export function PreviewPane({
projectId,
version,
documentStatus,
}: PreviewPaneProps) {
const pagesRef = useRef<HTMLDivElement>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const [state, setState] = useState<PreviewState>("loading");
const [state, setState] = useState<RenderState>("blank");

useEffect(() => {
let cancelled = false;
Expand All @@ -26,9 +33,9 @@ export function PreviewPane({ projectId, version }: PreviewPaneProps) {
const scroller = scrollRef.current;
if (!container || !scroller) return;

// nothing has been compiled yet on first open
// no successful compile yet, so there's no PDF to render
if (version === 0) {
setState("empty");
setState("blank");
return;
}

Expand Down Expand Up @@ -92,24 +99,30 @@ export function PreviewPane({ projectId, version }: PreviewPaneProps) {
};
}, [projectId, version]);

// pick the placeholder message for when there's no rendered PDF on screen
const placeholder = (() => {
if (documentStatus === "running") return null; // show the spinner instead
if (documentStatus === "empty")
return "This document is empty. Start writing to see the preview.";
if (documentStatus === "error" || state === "error")
return "No preview yet. Check the compile log for errors.";
return "Compile to see the preview.";
})();

const showOverlay = state !== "ready";
const showSpinner =
state === "loading" || (state === "blank" && documentStatus === "running");

return (
<div ref={scrollRef} className="relative h-full overflow-auto bg-surface-2">
<div ref={pagesRef} className="p-6" />

{state !== "ready" && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
{state === "loading" && (
{showOverlay && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center p-6 text-center">
{showSpinner ? (
<span className="h-6 w-6 animate-spin rounded-full border-2 border-accent border-t-transparent" />
)}
{state === "empty" && (
<p className="text-sm text-text-muted">
Compile to see the preview.
</p>
)}
{state === "error" && (
<p className="text-sm text-text-muted">
No preview yet. Check the compile log for errors.
</p>
) : (
<p className="max-w-xs text-sm text-text-muted">{placeholder}</p>
)}
</div>
)}
Expand Down
2 changes: 2 additions & 0 deletions src/components/editor/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ function statusLabel(
};
case "error":
return { text: "Compile errors", className: "text-danger" };
case "empty":
return { text: "Empty document", className: "text-text-muted" };
default:
return { text: "", className: "" };
}
Expand Down
11 changes: 10 additions & 1 deletion src/components/editor/useCompile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

import { useCallback, useRef, useState } from "react";

export type CompileStatus = "idle" | "running" | "success" | "error";
export type CompileStatus =
| "idle"
| "running"
| "success"
| "error"
| "empty";

// Drives compilation for one project. Reads the Server-Sent Events stream from
// the compile route, accumulating the log and learning when the PDF is ready.
Expand All @@ -22,6 +27,7 @@ export function useCompile(projectId: string) {
setLog("");

let success = false;
let empty = false;
let duration: number | null = null;

try {
Expand Down Expand Up @@ -57,6 +63,7 @@ export function useCompile(projectId: string) {
else if (event.type === "error") setLog((prev) => prev + event.message);
else if (event.type === "done") {
success = event.success;
empty = event.empty;
duration = event.durationMs;
}
}
Expand All @@ -70,6 +77,8 @@ export function useCompile(projectId: string) {
if (success) {
setStatus("success");
setPdfVersion((v) => v + 1);
} else if (empty) {
setStatus("empty");
} else {
setStatus("error");
}
Expand Down
11 changes: 9 additions & 2 deletions src/lib/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { mainSourcePath, outputPdfPath, ensureProjectDirs } from "./storage";

export interface CompileResult {
success: boolean;
// the document compiled cleanly but had nothing to typeset (an empty body).
// not a failure, just nothing to preview yet.
empty: boolean;
log: string;
durationMs: number;
passes: number;
Expand Down Expand Up @@ -66,14 +69,14 @@ export async function runCompile(
} catch {
const message = `Unknown engine: ${engineId}\n`;
onLog?.(message);
return { success: false, log: message, durationMs: 0, passes: 0 };
return { success: false, empty: false, log: message, durationMs: 0, passes: 0 };
}

const mainPath = mainSourcePath(projectId, engine);
if (!fs.existsSync(mainPath)) {
const message = `No ${engine.mainFileName} found for this project.\n`;
onLog?.(message);
return { success: false, log: message, durationMs: 0, passes: 0 };
return { success: false, empty: false, log: message, durationMs: 0, passes: 0 };
}

ensureProjectDirs(projectId);
Expand All @@ -99,8 +102,11 @@ export async function runCompile(
}

const pdfExists = fs.existsSync(outputPdfPath(projectId, engine));
// an empty body makes xelatex finish cleanly with no PDF and this notice
const empty = !pdfExists && /no pages of output/i.test(log);
return {
success: lastCode === 0 && pdfExists,
empty,
log,
durationMs: Date.now() - start,
passes,
Expand Down Expand Up @@ -132,6 +138,7 @@ export function compileStream(
send({
type: "done",
success: result.success,
empty: result.empty,
durationMs: result.durationMs,
passes: result.passes,
});
Expand Down
Loading