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

View file

@ -1,7 +1,7 @@
"use client"; "use client";
import { useAtomValue, useSetAtom } from "jotai"; import { useAtomValue, useSetAtom } from "jotai";
import { ChevronDownIcon, XIcon } from "lucide-react"; import { ChevronDownIcon, Pencil, XIcon } from "lucide-react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@ -125,6 +125,7 @@ export function ReportPanelContent({
// Editor state — tracks the latest markdown from the Plate editor // Editor state — tracks the latest markdown from the Plate editor
const [editedMarkdown, setEditedMarkdown] = useState<string | null>(null); const [editedMarkdown, setEditedMarkdown] = useState<string | null>(null);
const [isEditing, setIsEditing] = useState(false);
// Read-only when public (shareToken) OR shared (SEARCH_SPACE visibility) // Read-only when public (shareToken) OR shared (SEARCH_SPACE visibility)
const currentThreadState = useAtomValue(currentThreadAtom); const currentThreadState = useAtomValue(currentThreadAtom);
@ -188,6 +189,7 @@ export function ReportPanelContent({
// Reset edited markdown when switching versions or reports // Reset edited markdown when switching versions or reports
useEffect(() => { useEffect(() => {
setEditedMarkdown(null); setEditedMarkdown(null);
setIsEditing(false);
}, [activeReportId]); }, [activeReportId]);
// Copy markdown content (uses latest editor content) // Copy markdown content (uses latest editor content)
@ -257,7 +259,7 @@ export function ReportPanelContent({
// Save edited report content // Save edited report content
const handleSave = useCallback(async () => { const handleSave = useCallback(async () => {
if (!currentMarkdown || !activeReportId) return; if (!currentMarkdown || !activeReportId) return false;
setSaving(true); setSaving(true);
try { try {
const response = await authenticatedFetch( const response = await authenticatedFetch(
@ -278,9 +280,11 @@ export function ReportPanelContent({
setReportContent((prev) => (prev ? { ...prev, content: currentMarkdown } : prev)); setReportContent((prev) => (prev ? { ...prev, content: currentMarkdown } : prev));
setEditedMarkdown(null); setEditedMarkdown(null);
toast.success("Report saved successfully"); toast.success("Report saved successfully");
return true;
} catch (err) { } catch (err) {
console.error("Error saving report:", err); console.error("Error saving report:", err);
toast.error(err instanceof Error ? err.message : "Failed to save report"); toast.error(err instanceof Error ? err.message : "Failed to save report");
return false;
} finally { } finally {
setSaving(false); setSaving(false);
} }
@ -289,6 +293,14 @@ export function ReportPanelContent({
const activeVersionIndex = versions.findIndex((v) => v.id === activeReportId); const activeVersionIndex = versions.findIndex((v) => v.id === activeReportId);
const isPublic = !!shareToken; const isPublic = !!shareToken;
const btnBg = isPublic ? "bg-main-panel" : "bg-sidebar"; 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 ( return (
<> <>
@ -383,6 +395,58 @@ export function ReportPanelContent({
)} )}
</div> </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 */} {/* Report content — skeleton/error/viewer/editor shown only in this area */}
<div className="flex-1 overflow-hidden"> <div className="flex-1 overflow-hidden">
{isLoading ? ( {isLoading ? (
@ -406,15 +470,15 @@ export function ReportPanelContent({
</div> </div>
) : ( ) : (
<PlateEditor <PlateEditor
key={`report-${activeReportId}-${isEditing ? "editing" : "viewing"}`}
preset="full" preset="full"
markdown={reportContent.content} markdown={reportContent.content}
onMarkdownChange={setEditedMarkdown} onMarkdownChange={setEditedMarkdown}
readOnly={false} readOnly={!isEditing}
placeholder="Report content..." placeholder="Report content..."
editorVariant="default" editorVariant="default"
onSave={handleSave} allowModeToggle={false}
hasUnsavedChanges={editedMarkdown !== null} defaultEditing={isEditing}
isSaving={saving}
className="[&_[role=toolbar]]:!bg-sidebar" className="[&_[role=toolbar]]:!bg-sidebar"
/> />
) )