mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 02:23:47 +00:00
refactor: migrate to eslint flat config and format codebase with prettier
- Remove legacy .eslintrc.cjs and .eslintignore - Add eslint.config.js with flat config, security rules (no-eval, no-implied-eval, no-new-func, no-script-url) - Add .prettierrc and .prettierignore - Format entire codebase with prettier
This commit is contained in:
@@ -1,146 +1,147 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface OnboardingStep {
|
||||
target: string // data-onboarding attribute value
|
||||
title: string
|
||||
description: string
|
||||
placement: 'top' | 'bottom' | 'left' | 'right'
|
||||
target: string; // data-onboarding attribute value
|
||||
title: string;
|
||||
description: string;
|
||||
placement: 'top' | 'bottom' | 'left' | 'right';
|
||||
}
|
||||
|
||||
interface OnboardingProps {
|
||||
steps: OnboardingStep[]
|
||||
onComplete: () => void
|
||||
onSkip: () => void
|
||||
steps: OnboardingStep[];
|
||||
onComplete: () => void;
|
||||
onSkip: () => void;
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'onboarding_completed'
|
||||
const STORAGE_KEY = 'onboarding_completed';
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function useOnboarding() {
|
||||
const [isCompleted, setIsCompleted] = useState(() => {
|
||||
return localStorage.getItem(STORAGE_KEY) === 'true'
|
||||
})
|
||||
return localStorage.getItem(STORAGE_KEY) === 'true';
|
||||
});
|
||||
|
||||
const complete = useCallback(() => {
|
||||
localStorage.setItem(STORAGE_KEY, 'true')
|
||||
setIsCompleted(true)
|
||||
}, [])
|
||||
localStorage.setItem(STORAGE_KEY, 'true');
|
||||
setIsCompleted(true);
|
||||
}, []);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
setIsCompleted(false)
|
||||
}, [])
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
setIsCompleted(false);
|
||||
}, []);
|
||||
|
||||
return { isCompleted, complete, reset }
|
||||
return { isCompleted, complete, reset };
|
||||
}
|
||||
|
||||
export default function Onboarding({ steps, onComplete, onSkip }: OnboardingProps) {
|
||||
const { t } = useTranslation()
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
const [targetRect, setTargetRect] = useState<DOMRect | null>(null)
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
const tooltipRef = useRef<HTMLDivElement>(null)
|
||||
const { t } = useTranslation();
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [targetRect, setTargetRect] = useState<DOMRect | null>(null);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const step = steps[currentStep]
|
||||
const step = steps[currentStep];
|
||||
|
||||
// Find and highlight target element
|
||||
useEffect(() => {
|
||||
const findTarget = () => {
|
||||
const target = document.querySelector(`[data-onboarding="${step.target}"]`)
|
||||
const target = document.querySelector(`[data-onboarding="${step.target}"]`);
|
||||
if (target) {
|
||||
const rect = target.getBoundingClientRect()
|
||||
setTargetRect(rect)
|
||||
const rect = target.getBoundingClientRect();
|
||||
setTargetRect(rect);
|
||||
|
||||
// Scroll element into view if needed
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
|
||||
// Delay visibility for smooth animation
|
||||
setTimeout(() => setIsVisible(true), 100)
|
||||
setTimeout(() => setIsVisible(true), 100);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setIsVisible(false)
|
||||
const timer = setTimeout(findTarget, 300)
|
||||
return () => clearTimeout(timer)
|
||||
}, [step.target])
|
||||
setIsVisible(false);
|
||||
const timer = setTimeout(findTarget, 300);
|
||||
return () => clearTimeout(timer);
|
||||
}, [step.target]);
|
||||
|
||||
// Recalculate position on resize/scroll
|
||||
useEffect(() => {
|
||||
const updatePosition = () => {
|
||||
const target = document.querySelector(`[data-onboarding="${step.target}"]`)
|
||||
const target = document.querySelector(`[data-onboarding="${step.target}"]`);
|
||||
if (target) {
|
||||
setTargetRect(target.getBoundingClientRect())
|
||||
setTargetRect(target.getBoundingClientRect());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('resize', updatePosition)
|
||||
window.addEventListener('scroll', updatePosition, true)
|
||||
window.addEventListener('resize', updatePosition);
|
||||
window.addEventListener('scroll', updatePosition, true);
|
||||
return () => {
|
||||
window.removeEventListener('resize', updatePosition)
|
||||
window.removeEventListener('scroll', updatePosition, true)
|
||||
}
|
||||
}, [step.target])
|
||||
window.removeEventListener('resize', updatePosition);
|
||||
window.removeEventListener('scroll', updatePosition, true);
|
||||
};
|
||||
}, [step.target]);
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentStep < steps.length - 1) {
|
||||
setCurrentStep(currentStep + 1)
|
||||
setCurrentStep(currentStep + 1);
|
||||
} else {
|
||||
onComplete()
|
||||
onComplete();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrev = () => {
|
||||
if (currentStep > 0) {
|
||||
setCurrentStep(currentStep - 1)
|
||||
setCurrentStep(currentStep - 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSkip = () => {
|
||||
onSkip()
|
||||
}
|
||||
onSkip();
|
||||
};
|
||||
|
||||
// Calculate tooltip position
|
||||
const getTooltipStyle = (): React.CSSProperties => {
|
||||
if (!targetRect) return { opacity: 0 }
|
||||
if (!targetRect) return { opacity: 0 };
|
||||
|
||||
const padding = 16
|
||||
const tooltipWidth = 320
|
||||
const tooltipHeight = tooltipRef.current?.offsetHeight || 150
|
||||
const padding = 16;
|
||||
const tooltipWidth = 320;
|
||||
const tooltipHeight = tooltipRef.current?.offsetHeight || 150;
|
||||
|
||||
let top = 0
|
||||
let left = 0
|
||||
let top = 0;
|
||||
let left = 0;
|
||||
|
||||
switch (step.placement) {
|
||||
case 'bottom':
|
||||
top = targetRect.bottom + padding
|
||||
left = targetRect.left + targetRect.width / 2 - tooltipWidth / 2
|
||||
break
|
||||
top = targetRect.bottom + padding;
|
||||
left = targetRect.left + targetRect.width / 2 - tooltipWidth / 2;
|
||||
break;
|
||||
case 'top':
|
||||
top = targetRect.top - tooltipHeight - padding
|
||||
left = targetRect.left + targetRect.width / 2 - tooltipWidth / 2
|
||||
break
|
||||
top = targetRect.top - tooltipHeight - padding;
|
||||
left = targetRect.left + targetRect.width / 2 - tooltipWidth / 2;
|
||||
break;
|
||||
case 'left':
|
||||
top = targetRect.top + targetRect.height / 2 - tooltipHeight / 2
|
||||
left = targetRect.left - tooltipWidth - padding
|
||||
break
|
||||
top = targetRect.top + targetRect.height / 2 - tooltipHeight / 2;
|
||||
left = targetRect.left - tooltipWidth - padding;
|
||||
break;
|
||||
case 'right':
|
||||
top = targetRect.top + targetRect.height / 2 - tooltipHeight / 2
|
||||
left = targetRect.right + padding
|
||||
break
|
||||
top = targetRect.top + targetRect.height / 2 - tooltipHeight / 2;
|
||||
left = targetRect.right + padding;
|
||||
break;
|
||||
}
|
||||
|
||||
// Keep within viewport
|
||||
const viewportWidth = window.innerWidth
|
||||
const viewportHeight = window.innerHeight
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
if (left < padding) left = padding
|
||||
if (left < padding) left = padding;
|
||||
if (left + tooltipWidth > viewportWidth - padding) {
|
||||
left = viewportWidth - tooltipWidth - padding
|
||||
left = viewportWidth - tooltipWidth - padding;
|
||||
}
|
||||
if (top < padding) top = padding
|
||||
if (top < padding) top = padding;
|
||||
if (top + tooltipHeight > viewportHeight - padding) {
|
||||
top = viewportHeight - tooltipHeight - padding
|
||||
top = viewportHeight - tooltipHeight - padding;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -149,22 +150,22 @@ export default function Onboarding({ steps, onComplete, onSkip }: OnboardingProp
|
||||
width: tooltipWidth,
|
||||
opacity: isVisible ? 1 : 0,
|
||||
transform: isVisible ? 'scale(1)' : 'scale(0.95)',
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Spotlight style
|
||||
const getSpotlightStyle = (): React.CSSProperties => {
|
||||
if (!targetRect) return { opacity: 0 }
|
||||
if (!targetRect) return { opacity: 0 };
|
||||
|
||||
const padding = 8
|
||||
const padding = 8;
|
||||
return {
|
||||
top: targetRect.top - padding,
|
||||
left: targetRect.left - padding,
|
||||
width: targetRect.width + padding * 2,
|
||||
height: targetRect.height + padding * 2,
|
||||
opacity: isVisible ? 1 : 0,
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
<div className="onboarding-overlay" style={{ opacity: isVisible ? 1 : 0 }}>
|
||||
@@ -178,7 +179,7 @@ export default function Onboarding({ steps, onComplete, onSkip }: OnboardingProp
|
||||
style={getTooltipStyle()}
|
||||
>
|
||||
{/* Progress indicator */}
|
||||
<div className="flex items-center gap-1.5 mb-4">
|
||||
<div className="mb-4 flex items-center gap-1.5">
|
||||
{steps.map((s, index) => (
|
||||
<div
|
||||
key={s.target}
|
||||
@@ -186,33 +187,33 @@ export default function Onboarding({ steps, onComplete, onSkip }: OnboardingProp
|
||||
index === currentStep
|
||||
? 'w-6 bg-accent-500'
|
||||
: index < currentStep
|
||||
? 'w-2 bg-accent-500/50'
|
||||
: 'w-2 bg-dark-700'
|
||||
? 'w-2 bg-accent-500/50'
|
||||
: 'w-2 bg-dark-700'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<h3 className="text-lg font-semibold text-dark-50 mb-2">{step.title}</h3>
|
||||
<p className="text-dark-400 text-sm mb-5">{step.description}</p>
|
||||
<h3 className="mb-2 text-lg font-semibold text-dark-50">{step.title}</h3>
|
||||
<p className="mb-5 text-sm text-dark-400">{step.description}</p>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
onClick={handleSkip}
|
||||
className="text-dark-500 hover:text-dark-300 text-sm transition-colors"
|
||||
className="text-sm text-dark-500 transition-colors hover:text-dark-300"
|
||||
>
|
||||
{t('onboarding.skip', 'Skip')}
|
||||
</button>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{currentStep > 0 && (
|
||||
<button onClick={handlePrev} className="btn-ghost text-sm px-3 py-1.5">
|
||||
<button onClick={handlePrev} className="btn-ghost px-3 py-1.5 text-sm">
|
||||
{t('common.back', 'Back')}
|
||||
</button>
|
||||
)}
|
||||
<button onClick={handleNext} className="btn-primary text-sm px-4 py-1.5">
|
||||
<button onClick={handleNext} className="btn-primary px-4 py-1.5 text-sm">
|
||||
{currentStep === steps.length - 1
|
||||
? t('onboarding.finish', 'Finish')
|
||||
: t('common.next', 'Next')}
|
||||
@@ -235,6 +236,6 @@ export default function Onboarding({ steps, onComplete, onSkip }: OnboardingProp
|
||||
/>
|
||||
)}
|
||||
</div>,
|
||||
document.body
|
||||
)
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user