175 lines
5.8 KiB
TypeScript
175 lines
5.8 KiB
TypeScript
import React, { useState, useEffect, useCallback } from 'react';
|
|
import { X, ChevronRight, ChevronLeft } from 'lucide-react';
|
|
import { useLanguage } from '../contexts/LanguageContext';
|
|
|
|
interface Step {
|
|
id: string;
|
|
title: string;
|
|
content: string;
|
|
position: 'top' | 'bottom' | 'left' | 'right';
|
|
}
|
|
|
|
export default function UserGuide({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
|
|
const { t } = useLanguage();
|
|
const [currentStep, setCurrentStep] = useState(0);
|
|
const [highlightStyle, setHighlightStyle] = useState<React.CSSProperties>({});
|
|
|
|
const steps: Step[] = [
|
|
{
|
|
id: 'sidebar-upload-area',
|
|
title: t.guide.step1Title,
|
|
content: t.guide.step1Content,
|
|
position: 'right',
|
|
},
|
|
{
|
|
id: 'sidebar-history',
|
|
title: t.guide.step2Title,
|
|
content: t.guide.step2Content,
|
|
position: 'right',
|
|
},
|
|
{
|
|
id: 'file-preview-empty',
|
|
title: t.guide.step3Title,
|
|
content: t.guide.step3Content,
|
|
position: 'right',
|
|
},
|
|
{
|
|
id: 'result-empty-state',
|
|
title: t.guide.step4Title,
|
|
content: t.guide.step4Content,
|
|
position: 'left',
|
|
},
|
|
{
|
|
id: 'export-button',
|
|
title: t.guide.stepExportTitle,
|
|
content: t.guide.stepExportContent,
|
|
position: 'left',
|
|
},
|
|
];
|
|
|
|
const updateHighlight = useCallback(() => {
|
|
if (!isOpen || steps.length === 0) return;
|
|
|
|
const element = document.getElementById(steps[currentStep].id);
|
|
if (element) {
|
|
const rect = element.getBoundingClientRect();
|
|
setHighlightStyle({
|
|
top: rect.top - 8,
|
|
left: rect.left - 8,
|
|
width: rect.width + 16,
|
|
height: rect.height + 16,
|
|
opacity: 1,
|
|
});
|
|
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
} else {
|
|
setHighlightStyle({ opacity: 0 });
|
|
}
|
|
}, [currentStep, isOpen, steps, t.guide]);
|
|
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
updateHighlight();
|
|
window.addEventListener('resize', updateHighlight);
|
|
}
|
|
return () => window.removeEventListener('resize', updateHighlight);
|
|
}, [isOpen, updateHighlight]);
|
|
|
|
if (!isOpen) return null;
|
|
|
|
const handleNext = () => {
|
|
if (currentStep < steps.length - 1) {
|
|
setCurrentStep(currentStep + 1);
|
|
} else {
|
|
onClose();
|
|
setCurrentStep(0);
|
|
}
|
|
};
|
|
|
|
const handlePrev = () => {
|
|
if (currentStep > 0) {
|
|
setCurrentStep(currentStep - 1);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-[100] pointer-events-none">
|
|
{/* Backdrop with hole */}
|
|
<div className="absolute inset-0 bg-black/60 pointer-events-auto" onClick={onClose} style={{
|
|
clipPath: highlightStyle.top !== undefined ? `polygon(
|
|
0% 0%, 0% 100%,
|
|
${highlightStyle.left}px 100%,
|
|
${highlightStyle.left}px ${highlightStyle.top}px,
|
|
${(highlightStyle.left as number) + (highlightStyle.width as number)}px ${highlightStyle.top}px,
|
|
${(highlightStyle.left as number) + (highlightStyle.width as number)}px ${(highlightStyle.top as number) + (highlightStyle.height as number)}px,
|
|
${highlightStyle.left}px ${(highlightStyle.top as number) + (highlightStyle.height as number)}px,
|
|
${highlightStyle.left}px 100%,
|
|
100% 100%, 100% 0%
|
|
)` : 'none'
|
|
}} />
|
|
|
|
{/* Highlight border */}
|
|
<div
|
|
className="absolute border-2 border-blue-500 rounded-xl transition-all duration-300 shadow-[0_0_0_9999px_rgba(0,0,0,0.5)]"
|
|
style={highlightStyle}
|
|
/>
|
|
|
|
{/* Tooltip */}
|
|
<div
|
|
className="absolute pointer-events-auto bg-white rounded-xl shadow-2xl p-6 w-80 transition-all duration-300 animate-in fade-in zoom-in-95"
|
|
style={highlightStyle.top !== undefined ? {
|
|
top: steps[currentStep].position === 'bottom'
|
|
? (highlightStyle.top as number) + (highlightStyle.height as number) + 16
|
|
: steps[currentStep].position === 'top'
|
|
? (highlightStyle.top as number) - 200 // approximation
|
|
: (highlightStyle.top as number),
|
|
left: steps[currentStep].position === 'right'
|
|
? (highlightStyle.left as number) + (highlightStyle.width as number) + 16
|
|
: steps[currentStep].position === 'left'
|
|
? (highlightStyle.left as number) - 336
|
|
: (highlightStyle.left as number),
|
|
} : {
|
|
top: '50%',
|
|
left: '50%',
|
|
transform: 'translate(-50%, -50%)',
|
|
}}
|
|
>
|
|
<button
|
|
onClick={onClose}
|
|
className="absolute top-4 right-4 text-gray-400 hover:text-gray-600"
|
|
>
|
|
<X size={18} />
|
|
</button>
|
|
|
|
<div className="mb-4">
|
|
<span className="text-xs font-bold text-blue-600 uppercase tracking-wider">
|
|
Step {currentStep + 1} of {steps.length}
|
|
</span>
|
|
<h3 className="text-lg font-bold text-gray-900 mt-1">{steps[currentStep].title}</h3>
|
|
<p className="text-sm text-gray-600 mt-2 leading-relaxed">
|
|
{steps[currentStep].content}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between mt-6">
|
|
<button
|
|
onClick={handlePrev}
|
|
disabled={currentStep === 0}
|
|
className={`flex items-center gap-1 text-sm font-medium ${currentStep === 0 ? 'text-gray-300 cursor-not-allowed' : 'text-gray-600 hover:text-gray-900'}`}
|
|
>
|
|
<ChevronLeft size={16} />
|
|
{t.guide.prev}
|
|
</button>
|
|
|
|
<button
|
|
onClick={handleNext}
|
|
className="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors flex items-center gap-1"
|
|
>
|
|
{currentStep === steps.length - 1 ? t.guide.finish : t.guide.next}
|
|
{currentStep < steps.length - 1 && <ChevronRight size={16} />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|