feat: add translate
This commit is contained in:
174
src/components/UserGuide.tsx
Normal file
174
src/components/UserGuide.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user