diff --git a/src/components/FileUpload.tsx b/src/components/FileUpload.tsx index 8b931a22..0f9413fb 100644 --- a/src/components/FileUpload.tsx +++ b/src/components/FileUpload.tsx @@ -179,7 +179,6 @@ function DropZone({ }: DropZoneProps) { return (
+
{error && (

{error} diff --git a/src/components/OnboardingTour.tsx b/src/components/OnboardingTour.tsx index 861ab7de..adcf2c4f 100644 --- a/src/components/OnboardingTour.tsx +++ b/src/components/OnboardingTour.tsx @@ -13,6 +13,10 @@ interface TourStep { requiresFile?: boolean; } +interface OnboardingTourProps { + replayTrigger?: number; +} + const TOUR_STEPS: TourStep[] = [ { targetId: "upload-zone", @@ -249,7 +253,9 @@ function Tooltip({ ); } -export default function OnboardingTour() { +export default function OnboardingTour({ + replayTrigger = 0, +}: OnboardingTourProps) { const [stepIndex, setStepIndex] = useState(0); const [visible, setVisible] = useState(false); const [targetRect, setTargetRect] = useState(null); @@ -302,6 +308,24 @@ export default function OnboardingTour() { return () => clearTimeout(t); }, [measureTarget]); + useEffect(() => { + if (replayTrigger === 0) return; + + const replayTour = async () => { + localStorage.removeItem(TOUR_KEY); + setStepIndex(0); + + const rect = await measureTarget(TOUR_STEPS[0]?.targetId ?? ""); + + if (rect) { + setTargetRect(rect); + setVisible(true); + } + }; + + replayTour(); + }, [replayTrigger, measureTarget]); + // Measure target whenever step changes (skip on first render — init effect handles that) useEffect(() => { if (!visible) return; diff --git a/src/components/VideoEditor.tsx b/src/components/VideoEditor.tsx index 38045cf0..a7087a47 100644 --- a/src/components/VideoEditor.tsx +++ b/src/components/VideoEditor.tsx @@ -188,6 +188,10 @@ function KeyboardShortcutsPanel() { keys: [O], label: "Set trim out point", }, + { + keys: [T], + label: "Replay onboarding tour" + } ]; return ( @@ -273,6 +277,8 @@ export default function VideoEditor() { toggleSound, } = useVideoEditor(); + const [tourReplayTrigger, setTourReplayTrigger] = useState(0); + useKeyboardShortcuts({ file, recipe, @@ -282,6 +288,7 @@ export default function VideoEditor() { status, cancelExport, onToggleShortcutsModal: () => {}, + onReplayTour: () => setTourReplayTrigger((value) => value + 1), currentTime, duration, }); @@ -407,7 +414,7 @@ export default function VideoEditor() { exportStartedAt={exportStartedAt} onCancel={cancelExport} /> - +

{status === "exporting" && `Exporting video: ${progress}%`} diff --git a/src/hooks/useKeyboardShortcuts.ts b/src/hooks/useKeyboardShortcuts.ts index de87eab6..e1477eef 100644 --- a/src/hooks/useKeyboardShortcuts.ts +++ b/src/hooks/useKeyboardShortcuts.ts @@ -11,6 +11,7 @@ interface UseKeyboardShortcutsProps { status: ExportStatus; cancelExport: () => void; onToggleShortcutsModal: () => void; + onReplayTour: () => void; currentTime: number; duration: number; } @@ -24,6 +25,7 @@ export function useKeyboardShortcuts({ status, cancelExport, onToggleShortcutsModal, + onReplayTour, currentTime, duration, }: UseKeyboardShortcutsProps) { @@ -78,6 +80,10 @@ export function useKeyboardShortcuts({ case "O": updateRecipe({trimEnd: Math.floor(currentTimeRef.current)}); break; + case "t": + case "T": + onReplayTour(); + break; default: if (e.key >= "1" && e.key <= "9") { @@ -91,5 +97,5 @@ export function useKeyboardShortcuts({ window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); - }, [file, recipe, resetSettings, updateRecipe, handleExport, status, cancelExport, onToggleShortcutsModal, currentTime, duration]); + }, [file, recipe, resetSettings, updateRecipe, handleExport, status, cancelExport, onToggleShortcutsModal, onReplayTour, currentTime, duration]); }