Skip to main content

Overview

The triggers system detects specific user interactions and automatically unlocks corresponding achievements. It uses custom React hooks and browser event listeners to track behavior patterns.

Core Components

TriggersBoot.tsx

Initializes all trigger detection on application load:
import TriggersBoot from '@/components/gamification/TriggersBoot';

// In your root layout or page
<TriggersBoot />
This component sets up:
  • Scroll detection
  • Click tracking
  • Keyboard event listeners
  • Visibility tracking
  • Time-on-site monitoring

Hooks

useAchievmentsTrigger

Custom hook for achievement-specific trigger logic:
function useAchievmentsTrigger() {
  useEffect(() => {
    // Scroll trigger
    let scrolled = false;
    const handleScroll = () => {
      if (!scrolled && window.scrollY > 300) {
        scrolled = true;
        unlockAchievement('explorer');
      }
    };
    
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, []);
}

useMissionsTrigger

Tracks complex interaction patterns for missions:
function useMissionsTrigger() {
  useEffect(() => {
    let readingTime = 0;
    const interval = setInterval(() => {
      if (document.visibilityState === 'visible') {
        readingTime += 1;
        if (readingTime >= 15) {
          unlockAchievement('curious');
          clearInterval(interval);
        }
      }
    }, 1000);
    
    return () => clearInterval(interval);
  }, []);
}

Trigger Types

Scroll Triggers

Detect scroll-based exploration:
const handleScroll = () => {
  const scrollPercent = (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100;
  
  if (scrollPercent > 50 && !hasAchievement('explorer')) {
    unlockAchievement('explorer');
  }
};

window.addEventListener('scroll', handleScroll, { passive: true });
Scroll triggers use passive: true for better performance and include anti-programmatic-scroll detection.

Click Triggers

Track specific element interactions:
// In a component
const handleServiceClick = () => {
  const clicks = Number(sessionStorage.getItem('service_clicks') || '0');
  sessionStorage.setItem('service_clicks', String(clicks + 1));
  
  if (clicks + 1 >= 2) {
    unlockAchievement('services_decoded');
  }
};

<button onClick={handleServiceClick}>Learn More</button>

Time-Based Triggers

Unlock achievements based on dwell time:
let timeOnPage = 0;
const interval = setInterval(() => {
  if (document.visibilityState === 'visible') {
    timeOnPage += 1;
    
    if (timeOnPage >= 20) {
      unlockAchievement('observer');
      clearInterval(interval);
    }
  }
}, 1000);
Time triggers only count visible time when the tab is active, preventing inflation from background tabs.

Visibility Triggers

Detect when specific elements enter the viewport:
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      unlockAchievement('reached_footer');
      observer.disconnect();
    }
  });
}, { threshold: 0.5 });

const footer = document.querySelector('footer');
if (footer) observer.observe(footer);

Keyboard Triggers

Easter egg detection via key sequences:
let sequence = '';
const TARGET = 'GUIGOLO';

document.addEventListener('keydown', (e) => {
  sequence += e.key.toUpperCase();
  sequence = sequence.slice(-TARGET.length);
  
  if (sequence === TARGET) {
    unlockAchievement('curious_player');
  }
});

Form Triggers

Track contact form interaction:
// From Contact.tsx
const handleFormInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
  const message = e.target.value;
  
  if (message.length >= 20 && !hasAchievement('took_courage')) {
    unlockAchievement('took_courage');
  }
};

const handleFormSubmit = async (e: React.FormEvent) => {
  e.preventDefault();
  
  // Submit form...
  
  unlockAchievement('first_contact');
};

Anti-Spam Protection

Triggers include cooldown mechanisms:
const COOLDOWN_MS = 2500;
const cooldownKey = 'trigger_cooldown';

function triggerWithCooldown(achievementId: AchievementId) {
  const lastTrigger = Number(sessionStorage.getItem(cooldownKey) || '0');
  const now = Date.now();
  
  if (now - lastTrigger < COOLDOWN_MS) {
    return; // Too soon
  }
  
  sessionStorage.setItem(cooldownKey, String(now));
  unlockAchievement(achievementId);
}
Always include anti-spam measures to prevent rapid-fire unlocking, especially on mobile where scroll events fire frequently.

Programmatic Detection Prevention

Distinguish human from automated scrolling:
let lastScrollTime = 0;
let scrollCount = 0;

const handleScroll = () => {
  const now = Date.now();
  const timeDiff = now - lastScrollTime;
  
  // Detect smooth/instant scroll (likely programmatic)
  if (timeDiff < 16) {
    scrollCount++;
    if (scrollCount > 10) {
      return; // Likely automated
    }
  } else {
    scrollCount = 0;
  }
  
  lastScrollTime = now;
  
  // Process as human scroll
  if (window.scrollY > 300) {
    unlockAchievement('explorer');
  }
};

Session Storage Usage

Triggers use sessionStorage for temporary state:
// Track within-session interactions
sessionStorage.setItem('service_clicks', '2');
sessionStorage.setItem('project_views', '3');
sessionStorage.setItem('time_on_page', '45');
SessionStorage clears on tab close, making it perfect for tracking temporary interaction counts without polluting localStorage.

Custom Events

Triggers can emit custom events for UI updates:
import { emitAchievementUnlocked } from '@/components/gamification/achievementEvents';

// Called automatically by unlockAchievement()
emitAchievementUnlocked({ id: 'explorer', at: Date.now() });

// Listen in UI components
import { onAchievementUnlocked } from '@/components/gamification/achievementEvents';

onAchievementUnlocked((achievement) => {
  // Show toast notification
  toast.success(achievement.id);
});

Best Practices

Use passive: true on scroll and touch event listeners to improve performance:
window.addEventListener('scroll', handler, { passive: true });
Clean up event listeners and intervals in useEffect cleanup functions to prevent memory leaks:
useEffect(() => {
  const handler = () => {};
  window.addEventListener('scroll', handler);
  return () => window.removeEventListener('scroll', handler);
}, []);
Triggers should feel natural and reward genuine engagement, not arbitrary actions. Test on real users to ensure thresholds make sense.

Build docs developers (and LLMs) love