refactor(editor): replace Loader2 with Spinner component and enhance save button visibility

This commit is contained in:
Anish Sarkar 2026-04-23 20:03:18 +05:30
parent 06b509213c
commit 0381632bc2
2 changed files with 76 additions and 12 deletions

View file

@ -7,7 +7,6 @@ import {
Download,
FileQuestionMark,
FileText,
Loader2,
Pencil,
RefreshCw,
XIcon,
@ -22,6 +21,7 @@ import { MarkdownViewer } from "@/components/markdown-viewer";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/ui/drawer";
import { Spinner } from "@/components/ui/spinner";
import { useMediaQuery } from "@/hooks/use-media-query";
import { useElectronAPI } from "@/hooks/use-platform";
import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils";
@ -346,8 +346,8 @@ export function EditorPanelContent({
}}
disabled={saving || !hasUnsavedChanges}
>
<span className={saving ? "invisible" : ""}>Save</span>
{saving && <Loader2 className="absolute size-3 animate-spin" />}
<span className={saving ? "opacity-0" : ""}>Save</span>
{saving && <Spinner size="xs" className="absolute" />}
</Button>
</>
) : (
@ -416,8 +416,8 @@ export function EditorPanelContent({
}}
disabled={saving || !hasUnsavedChanges}
>
<span className={saving ? "invisible" : ""}>Save</span>
{saving && <Loader2 className="absolute size-3 animate-spin" />}
<span className={saving ? "opacity-0" : ""}>Save</span>
{saving && <Spinner size="xs" className="absolute" />}
</Button>
</>
) : (
@ -534,7 +534,7 @@ export function EditorPanelContent({
}}
>
{downloading ? (
<Loader2 className="size-3.5 animate-spin" />
<Spinner size="xs" />
) : (
<Download className="size-3.5" />
)}

View file

@ -1,7 +1,7 @@
"use client";
import { useAtomValue, useSetAtom } from "jotai";
import { ChevronDownIcon, XIcon } from "lucide-react";
import { ChevronDownIcon, Pencil, XIcon } from "lucide-react";
import dynamic from "next/dynamic";
import { useCallback, useEffect, useRef, useState } from "react";
import { toast } from "sonner";
@ -125,6 +125,7 @@ export function ReportPanelContent({
// Editor state — tracks the latest markdown from the Plate editor
const [editedMarkdown, setEditedMarkdown] = useState<string | null>(null);
const [isEditing, setIsEditing] = useState(false);
// Read-only when public (shareToken) OR shared (SEARCH_SPACE visibility)
const currentThreadState = useAtomValue(currentThreadAtom);
@ -188,6 +189,7 @@ export function ReportPanelContent({
// Reset edited markdown when switching versions or reports
useEffect(() => {
setEditedMarkdown(null);
setIsEditing(false);
}, [activeReportId]);
// Copy markdown content (uses latest editor content)
@ -257,7 +259,7 @@ export function ReportPanelContent({
// Save edited report content
const handleSave = useCallback(async () => {
if (!currentMarkdown || !activeReportId) return;
if (!currentMarkdown || !activeReportId) return false;
setSaving(true);
try {
const response = await authenticatedFetch(
@ -278,9 +280,11 @@ export function ReportPanelContent({
setReportContent((prev) => (prev ? { ...prev, content: currentMarkdown } : prev));
setEditedMarkdown(null);
toast.success("Report saved successfully");
return true;
} catch (err) {
console.error("Error saving report:", err);
toast.error(err instanceof Error ? err.message : "Failed to save report");
return false;
} finally {
setSaving(false);
}
@ -289,6 +293,14 @@ export function ReportPanelContent({
const activeVersionIndex = versions.findIndex((v) => v.id === activeReportId);
const isPublic = !!shareToken;
const btnBg = isPublic ? "bg-main-panel" : "bg-sidebar";
const isResume = reportContent?.content_type === "typst";
const showReportEditingTier = !isResume;
const hasUnsavedChanges = editedMarkdown !== null;
const handleCancelEditing = useCallback(() => {
setEditedMarkdown(null);
setIsEditing(false);
}, []);
return (
<>
@ -383,6 +395,58 @@ export function ReportPanelContent({
)}
</div>
{showReportEditingTier && (
<div className="flex h-10 items-center justify-between gap-2 border-t border-b px-4 shrink-0">
<div className="min-w-0 flex-1">
<p className="truncate text-sm text-muted-foreground">
{reportContent?.title || title}
</p>
</div>
<div className="flex items-center gap-1 shrink-0">
{!isReadOnly &&
(isEditing ? (
<>
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-xs"
onClick={handleCancelEditing}
disabled={saving}
>
Cancel
</Button>
<Button
variant="secondary"
size="sm"
className="relative h-6 w-[56px] px-0 text-xs"
onClick={async () => {
const saveSucceeded = await handleSave();
if (saveSucceeded) setIsEditing(false);
}}
disabled={saving || !hasUnsavedChanges}
>
<span className={saving ? "opacity-0" : ""}>Save</span>
{saving && <Spinner size="xs" className="absolute" />}
</Button>
</>
) : (
<Button
variant="ghost"
size="icon"
className="size-6"
onClick={() => {
setEditedMarkdown(null);
setIsEditing(true);
}}
>
<Pencil className="size-3.5" />
<span className="sr-only">Edit report</span>
</Button>
))}
</div>
</div>
)}
{/* Report content — skeleton/error/viewer/editor shown only in this area */}
<div className="flex-1 overflow-hidden">
{isLoading ? (
@ -406,15 +470,15 @@ export function ReportPanelContent({
</div>
) : (
<PlateEditor
key={`report-${activeReportId}-${isEditing ? "editing" : "viewing"}`}
preset="full"
markdown={reportContent.content}
onMarkdownChange={setEditedMarkdown}
readOnly={false}
readOnly={!isEditing}
placeholder="Report content..."
editorVariant="default"
onSave={handleSave}
hasUnsavedChanges={editedMarkdown !== null}
isSaving={saving}
allowModeToggle={false}
defaultEditing={isEditing}
className="[&_[role=toolbar]]:!bg-sidebar"
/>
)