add servies page with content and a typing words feature to contact page
This commit is contained in:
84
components/typing-words.tsx
Normal file
84
components/typing-words.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import './typing-words.css';
|
||||
|
||||
const WORDS = ['plan', 'idea', 'problem', 'goals'] as const;
|
||||
|
||||
const TYPING_MS = 90;
|
||||
const DELETING_MS = 55;
|
||||
const PAUSE_AFTER_TYPED_MS = 2000;
|
||||
const PAUSE_AFTER_DELETED_MS = 400;
|
||||
|
||||
type Phase = 'typing' | 'pausing' | 'deleting' | 'waiting';
|
||||
|
||||
export default function TypingWords() {
|
||||
const [wordIndex, setWordIndex] = useState(0);
|
||||
const [charIndex, setCharIndex] = useState(0);
|
||||
const [phase, setPhase] = useState<Phase>('typing');
|
||||
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const media = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||
setPrefersReducedMotion(media.matches);
|
||||
|
||||
const onChange = () => setPrefersReducedMotion(media.matches);
|
||||
media.addEventListener('change', onChange);
|
||||
return () => media.removeEventListener('change', onChange);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (prefersReducedMotion) return;
|
||||
|
||||
const currentWord = WORDS[wordIndex];
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
|
||||
if (phase === 'typing') {
|
||||
if (charIndex < currentWord.length) {
|
||||
timeoutId = setTimeout(() => setCharIndex((i) => i + 1), TYPING_MS);
|
||||
} else {
|
||||
timeoutId = setTimeout(() => setPhase('pausing'), PAUSE_AFTER_TYPED_MS);
|
||||
}
|
||||
} else if (phase === 'pausing') {
|
||||
timeoutId = setTimeout(() => setPhase('deleting'), 0);
|
||||
} else if (phase === 'deleting') {
|
||||
if (charIndex > 0) {
|
||||
timeoutId = setTimeout(() => setCharIndex((i) => i - 1), DELETING_MS);
|
||||
} else {
|
||||
timeoutId = setTimeout(() => {
|
||||
setWordIndex((i) => (i + 1) % WORDS.length);
|
||||
setPhase('waiting');
|
||||
}, PAUSE_AFTER_DELETED_MS);
|
||||
}
|
||||
} else if (phase === 'waiting') {
|
||||
timeoutId = setTimeout(() => setPhase('typing'), 0);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [charIndex, phase, wordIndex, prefersReducedMotion]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!prefersReducedMotion) return;
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
setWordIndex((i) => (i + 1) % WORDS.length);
|
||||
}, 3000);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, [prefersReducedMotion]);
|
||||
|
||||
const displayedWord = prefersReducedMotion
|
||||
? WORDS[wordIndex]
|
||||
: WORDS[wordIndex].slice(0, charIndex);
|
||||
|
||||
return (
|
||||
<span className="typing-words" aria-live="polite">
|
||||
<span className="typing-words-text text-transparent bg-clip-text bg-gradient-to-r from-sky-400 to-violet-800">
|
||||
{displayedWord}
|
||||
</span>
|
||||
<span className="typing-words-cursor" aria-hidden="true">
|
||||
|
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user