kanban / src /components /editor /EditorHeader.tsx
Leon4gr45's picture
Deploy magic-resume as Docker Space (port 3000)
c30ea2a verified
Raw
History Blame Contribute Delete
7.28 kB
import { useEffect, useState } from "react";
import { useTranslations } from "@/i18n/compat/client";
import { AlertCircle, ShieldCheck, ShieldAlert, Edit2 } from "lucide-react";
import { motion } from "framer-motion";
import { useRouter } from "@/lib/navigation";
import { Input } from "@/components/ui/input";
import PdfExport from "../shared/PdfExport";
import ThemeToggle from "../shared/ThemeToggle";
import { useResumeStore } from "@/store/useResumeStore";
import { getThemeConfig } from "@/theme/themeConfig";
import { useGrammarCheck } from "@/hooks/useGrammarCheck";
import {
HoverCard,
HoverCardTrigger,
HoverCardContent
} from "@/components/ui/hover-card";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
import { GrammarCheckDrawer } from "./grammar/GrammarCheckDrawer";
import { getFileHandle, getConfig } from "@/utils/fileSystem";
interface EditorHeaderProps {
isMobile?: boolean;
}
export function EditorHeader({ isMobile }: EditorHeaderProps) {
const { activeResume, setActiveSection, updateResumeTitle } =
useResumeStore();
const { menuSections = [], activeSection } = activeResume || {};
const themeConfig = getThemeConfig();
const { errors, selectError } = useGrammarCheck();
const router = useRouter();
const t = useTranslations();
const visibleSections = menuSections
?.filter((section) => section.enabled)
.sort((a, b) => a.order - b.order);
const [backupConfigured, setBackupConfigured] = useState<boolean | null>(null);
const [backupPath, setBackupPath] = useState<string>("");
useEffect(() => {
const checkBackup = async () => {
try {
const handle = await getFileHandle("syncDirectory");
const path = await getConfig("syncDirectoryPath");
setBackupConfigured(!!handle);
setBackupPath(path || "");
} catch {
setBackupConfigured(false);
}
};
checkBackup();
}, []);
return (
<motion.header
className={`h-16 border-b sticky top-0 z-10`}
initial={{ y: -100 }}
animate={{ y: 0 }}
>
<div className="flex items-center justify-between px-6 h-full pr-2">
<div className="flex items-center space-x-4 scrollbar-hide">
<motion.div
className="flex items-center space-x-2 shrink-0 cursor-pointer"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={() => {
router.push("/app/dashboard");
}}
>
<span className="text-lg font-semibold">{t("common.title")}</span>
</motion.div>
<span className="text-muted-foreground/30 hidden md:inline-block font-light">/</span>
<div className="relative hidden md:flex items-center group">
<Input
key={activeResume?.id || "resume-title"}
defaultValue={activeResume?.title || ""}
onBlur={(e) => {
updateResumeTitle(e.target.value || "未命名简历");
}}
className="w-56 text-sm h-8 bg-muted/30 border-transparent hover:bg-muted/60 focus:bg-background transition-colors px-2.5 py-1 pr-8 shadow-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:border-border rounded-md font-medium text-foreground/90 hover:text-foreground"
placeholder="简历名称"
/>
<Edit2 className="w-3.5 h-3.5 absolute right-2.5 text-muted-foreground/40 pointer-events-none transition-colors group-hover:text-muted-foreground/80" />
</div>
{/* Backup Status Badge */}
{backupConfigured !== null && (
<TooltipProvider delayDuration={100}>
<Tooltip>
<TooltipTrigger asChild>
<motion.div
className={`
hidden md:flex items-center gap-1.5 px-2 py-1 rounded-md cursor-pointer
text-xs font-medium transition-colors ml-2
${backupConfigured
? "text-emerald-600/80 hover:bg-emerald-100/50 dark:text-emerald-500/80 dark:hover:bg-emerald-900/30"
: "text-amber-600/80 hover:bg-amber-100/50 dark:text-amber-500/80 dark:hover:bg-amber-900/30"
}
`}
initial={{ opacity: 0, x: -5 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.3, duration: 0.3 }}
onClick={() => router.push("/app/dashboard/settings")}
>
{backupConfigured ? (
<>
<ShieldCheck className="w-4 h-4" />
<span>{t("previewDock.backup.configured")}</span>
</>
) : (
<>
<motion.div
animate={{ scale: [1, 1.15, 1] }}
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
className="relative"
>
<ShieldAlert className="w-4 h-4" />
<span className="absolute -top-0.5 -right-0.5 w-1.5 h-1.5 rounded-full bg-amber-500" />
</motion.div>
<span>{t("previewDock.backup.notConfigured")}</span>
</>
)}
</motion.div>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={8} className="max-w-[240px]">
{backupConfigured ? (
<div className="flex flex-col gap-0.5">
<span className="text-xs font-medium">{t("previewDock.backup.configured")}</span>
<span className="text-[10px] text-muted-foreground truncate">{backupPath}</span>
</div>
) : (
<div className="flex flex-col gap-0.5">
<span className="text-xs font-medium">{t("previewDock.backup.notConfigured")}</span>
<span className="text-[10px] text-muted-foreground">{t("previewDock.backup.clickToConfigure")}</span>
</div>
)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
<div className="flex items-center space-x-3">
<GrammarCheckDrawer />
{errors.length > 0 && (
<div
className="flex items-center space-x-1 cursor-pointer animate-pulse"
onClick={() => document.dispatchEvent(new CustomEvent('open-grammar-drawer'))}
>
<AlertCircle className="w-4 h-4 text-red-500" />
<span className="text-sm text-red-500">
{t("grammarCheck.found_issues", { count: errors.length })}
</span>
</div>
)}
<ThemeToggle></ThemeToggle>
<div className="md:flex items-center ">
<PdfExport />
</div>
</div>
</div>
</motion.header>
);
}