Documentation Index
Fetch the complete documentation index at: https://mintlify.com/apursley2012/spooky-developer/llms.txt
Use this file to discover all available pages before exploring further.
HauntContext is the central nervous system of Spooky Developer’s interactivity. It exposes a single React context with five values that any component in the tree can consume via the useHaunt() hook. Understanding it is the key to customizing or extending any of the atmospheric effects.
Context Shape
interface HauntContextValue {
isHaunted: boolean; // Whether haunted effects are currently active
toggleHaunt: () => void; // Toggle isHaunted and persist the new value to localStorage
hasClicked: boolean; // Whether the user has clicked at least once this session
registerClick: () => void; // Mark the first click (enables SpiderScare)
mousePos: { x: number; y: number }; // Live cursor coordinates (clientX / clientY)
}
HauntProvider
HauntProvider is a standard React function component that wraps the application root. It owns all five values as useState / useEffect state and passes them through the context.
Initialization
On first mount, a single useEffect runs the following startup sequence:
useEffect(() => {
// 1. Restore haunted mode preference from localStorage
const stored = localStorage.getItem('haunt-enabled');
if (stored !== null) {
setIsHaunted(stored === 'true');
}
// defaults to true if key is absent (first-ever visit)
// 2. Restore first-click flag from sessionStorage
if (sessionStorage.getItem('haunt-clicked') === 'true') {
setHasClicked(true);
}
// 3. Start tracking cursor position
const handleMove = (e) => {
setMousePos({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
Key points:
isHaunted defaults to true if localStorage has no 'haunt-enabled' key. First-time visitors get the full haunted experience.
hasClicked is backed by sessionStorage, so a fresh tab starts with no first-click and the spider can drop once per session.
- The
mousemove listener is cleaned up in the effect’s return function, preventing memory leaks.
toggleHaunt
const toggleHaunt = () => {
const next = !isHaunted;
setIsHaunted(next);
localStorage.setItem('haunt-enabled', String(next));
};
Flips isHaunted and synchronously writes the new value to localStorage. This means:
- The toggle survives full page reloads.
- The effect on scare components (which gate on
isHaunted) is immediate — no re-mount required.
The toggle button is rendered in the Footer (components/layout/Footer.js). It reads both isHaunted and toggleHaunt from useHaunt() and displays “Awaken the spirits” or “Calm the haunt” depending on current state.
registerClick
const registerClick = () => {
if (!hasClicked) {
setHasClicked(true);
sessionStorage.setItem('haunt-clicked', 'true');
}
};
registerClick is idempotent — it only fires the state update on the very first call. Once hasClicked is true, all subsequent calls are no-ops. SpiderScare calls registerClick on the first click event it intercepts, which both marks the session flag and gives it the signal to drop the spider animation.
useHaunt() Hook
useHaunt is a thin wrapper around React.useContext that adds a guard against out-of-tree usage:
// assets/HauntContext.js
export const useHaunt = () => {
const ctx = React.useContext(HauntContext);
if (ctx === undefined) {
throw new Error('useHaunt must be used within a HauntProvider');
}
return ctx;
};
Usage Example
import { useHaunt } from './assets/HauntContext';
function MyComponent() {
const { isHaunted, toggleHaunt, mousePos } = useHaunt();
return (
<button onClick={toggleHaunt}>
{isHaunted ? 'Disable haunting' : 'Enable haunting'}
</button>
);
}
You can destructure only the values you need. Components that only need mousePos (like the pumpkin eye-tracking on the home page) simply ignore isHaunted and toggleHaunt.
useHaunt() must be called inside a component that is rendered within the HauntProvider tree. Calling it outside (e.g., in a component rendered before HauntProvider or in a completely separate React root) will throw:Error: useHaunt must be used within a HauntProvider
All page components and layout components are safe because HauntProvider is the outermost wrapper in App — but be careful if you add components rendered via portals or separate ReactDOM.render calls.
Provider Setup
HauntProvider wraps the entire application at the root, sitting outside BrowserRouter so that context is available regardless of routing state:
// assets/main.js — App component (simplified)
function App() {
return (
<HauntProvider>
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<HomePage />} />
<Route path="about" element={<AboutPage />} />
<Route path="projects" element={<ProjectsPage />} />
<Route path="skills" element={<SkillsPage />} />
<Route path="work" element={<WorkPage />} />
<Route path="case-studies" element={<CaseStudiesPage />} />
<Route path="articles" element={<ArticlesPage />} />
<Route path="contact" element={<ContactPage />} />
<Route path="testimonials" element={<TestimonialsPage />} />
<Route path="*" element={<NotFoundPage />} />
</Route>
</Routes>
</BrowserRouter>
</HauntProvider>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
Storage Summary
| Key | Storage | Default | Description |
|---|
haunt-enabled | localStorage | "true" | Persists haunted mode across sessions |
haunt-clicked | sessionStorage | absent | Tracks first click within a browser tab session |
localStorage is persistent — it survives closing and reopening the browser. sessionStorage is tab-scoped — it resets when the tab is closed. This combination means the spider can appear once per tab session regardless of how many times the user reloads the page.