diff --git a/TypingEffect.jsx b/TypingEffect.jsx new file mode 100644 index 0000000..434e6c2 --- /dev/null +++ b/TypingEffect.jsx @@ -0,0 +1,109 @@ +import React, { useState, useEffect } from 'react'; + +export default function TypingEffect({ + steps = [], + loop = true, + typingSpeed = [40, 120], + deleteSpeed = [20, 60], +}) { + const [displayed, setDisplayed] = useState(''); + const [showCursor, setShowCursor] = useState(true); + const [cursorBlink, setCursorBlink] = useState(true); + + const configKey = JSON.stringify({ steps, loop, typingSpeed, deleteSpeed }); + + useEffect(() => { + let cancelled = false; + let timerId = null; + + const sleep = (ms) => + new Promise((resolve) => { + timerId = setTimeout(resolve, ms); + }); + + const rand = ([min, max]) => + Math.floor(min + Math.random() * (max - min + 1)); + + const run = async () => { + do { + let chars = []; + setDisplayed(''); + + for (const step of steps) { + if (cancelled) return; + + if ('text' in step) { + const speed = step.speed || typingSpeed; + for (const c of Array.from(step.text)) { + if (cancelled) return; + chars.push(c); + setDisplayed(chars.join('')); + await sleep(rand(speed)); + } + } else if ('pause' in step) { + await sleep(step.pause); + } else if ('delete' in step) { + const speed = step.speed || deleteSpeed; + const count = + step.delete === 'all' + ? chars.length + : Math.min(step.delete, chars.length); + for (let i = 0; i < count; i++) { + if (cancelled) return; + chars.pop(); + setDisplayed(chars.join('')); + await sleep(rand(speed)); + } + } + } + + if (!loop) { + setShowCursor(false); + return; + } + + await sleep(500); + } while (!cancelled); + }; + + run(); + + return () => { + cancelled = true; + if (timerId != null) clearTimeout(timerId); + }; + }, [configKey]); + + useEffect(() => { + if (!showCursor) return; + const id = setInterval(() => setCursorBlink((v) => !v), 530); + return () => clearInterval(id); + }, [showCursor]); + + const parts = displayed.split('\n'); + + return ( + + {parts.map((part, i) => ( + + {i > 0 &&
} + {part} +
+ ))} + {showCursor && ( + + ); +}