Skip to main content

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

KeyStorageDefaultDescription
haunt-enabledlocalStorage"true"Persists haunted mode across sessions
haunt-clickedsessionStorageabsentTracks 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.

Build docs developers (and LLMs) love