Skip to main content

Overview

The achievements system tracks user interactions and milestones throughout the portfolio experience. It uses localStorage for persistence and emits custom events for UI notifications.

Core Types

AchievementId

Union type of all available achievement identifiers:
type AchievementId =
  | "first_step"          // Initial visit
  | "explorer"            // Scroll exploration
  | "curious"             // Reading content
  | "observer"            // Dwell time
  | "map_complete"        // Navigation exploration
  | "services_decoded"    // Services interaction
  | "projects_gallery"    // Project browsing
  | "read_between_lines"  // Deep content engagement
  | "critical_thinker"    // Analysis patterns
  | "good_eye"            // Visual attention
  | "curious_player"      // Easter egg discovery
  | "visual_match"        // Return visit
  | "almost_talked"       // Contact form view
  | "took_courage"        // Form interaction
  | "first_contact";      // Form submission

AchievementMeta

Metadata structure for achievement definitions:
id
AchievementId
required
Unique identifier for the achievement
title
string
required
Display name for the achievement
description
string
required
User-facing description shown when unlocked
icon
string
Path to achievement icon image
howItWorks
object
required
Technical documentation for the achievement:
  • trigger: What causes the achievement to unlock
  • behavior: User behavior being tracked
  • whyItMatters: Business/UX reasoning

AchievementsStateV1

The complete state object stored in localStorage:
type AchievementsStateV1 = {
  version: 1;
  unlocked: Partial<Record<AchievementId, UnlockedEntry>>;
  stats: {
    visits: number;
    firstVisitAt: number;
    lastVisitAt: number;
    lastSeenDate: string; // YYYY-MM-DD
  };
};

UnlockedEntry

type UnlockedEntry = {
  at: number;      // Timestamp when first unlocked
  count: number;   // For accumulative achievements
};

Functions

unlockAchievement()

Unlocks an achievement and updates localStorage.
id
AchievementId
required
The achievement to unlock
opts
object
Options object:
  • accumulative: If true, increments count on existing achievements
unlockedNow
boolean
True if this was the first time unlocking (false if already unlocked)
state
AchievementsStateV1
Updated achievements state
entry
UnlockedEntry
The achievement entry with timestamp and count
import { unlockAchievement } from '@/components/gamification/achievementsStore';

const result = unlockAchievement('first_step');
if (result.unlockedNow) {
  console.log('Achievement unlocked!');
}
The function includes a 2.5-second cooldown to prevent spam unlocking. The cooldown doesn’t apply to first_contact.

hasAchievement()

Checks if an achievement has been unlocked.
id
AchievementId
required
Achievement to check
import { hasAchievement } from '@/components/gamification/achievementsStore';

if (hasAchievement('explorer')) {
  // User has explored the site
}

trackVisit()

Tracks a page visit and updates visit statistics.
isNewDay
boolean
True if this is the first visit today (different from last visit date)
state
AchievementsStateV1
Updated state with new visit stats
import { trackVisit } from '@/components/gamification/achievementsStore';

const { isNewDay } = trackVisit();
if (isNewDay) {
  unlockAchievement('visual_match');
}

getAchievementsState()

Retrieves the current achievements state from localStorage.
import { getAchievementsState } from '@/components/gamification/achievementsStore';

const state = getAchievementsState();
console.log(`Total visits: ${state.stats.visits}`);

getUnlockedCount()

Returns the total number of unlocked achievements.
import { getUnlockedCount } from '@/components/gamification/achievementsStore';

const count = getUnlockedCount();
console.log(`Unlocked ${count} achievements`);

resetAchievements()

Clears all achievement data from localStorage. Useful for testing.
import { resetAchievements } from '@/components/gamification/achievementsStore';

resetAchievements();
This action is irreversible and will delete all user progress.

Achievement Catalog

The ACHIEVEMENTS array in achievementsCatalog.ts contains all 15 achievement definitions:
import { ACHIEVEMENTS, ACH_BY_ID } from '@/components/gamification/achievementsCatalog';

// Get all achievements
console.log(ACHIEVEMENTS.length); // 15

// Get specific achievement by ID
const achievement = ACH_BY_ID['first_step'];
console.log(achievement.title); // "Primer paso"

LocalStorage Schema

Achievements are stored at the key guigolo_achievements_v1:
{
  "version": 1,
  "unlocked": {
    "first_step": {
      "at": 1709584320000,
      "count": 1
    },
    "explorer": {
      "at": 1709584335000,
      "count": 1
    }
  },
  "stats": {
    "visits": 3,
    "firstVisitAt": 1709584320000,
    "lastVisitAt": 1709670720000,
    "lastSeenDate": "2024-03-05"
  }
}

Events

When an achievement is unlocked, a custom event is emitted:
// Listen for achievement unlocks
import { onAchievementUnlocked } from '@/components/gamification/achievementEvents';

onAchievementUnlocked((achievement) => {
  console.log(`Unlocked: ${achievement.id}`);
  // Show toast notification
});

Best Practices

Use hasAchievement() to check status before attempting complex unlock logic. It’s faster than calling getAchievementsState() directly.
The cooldown system prevents rapid-fire unlocking on mobile devices where scroll events can fire frequently. Adjust the COOLDOWN_MS constant if needed.
Achievement state persists across sessions using localStorage. Users who clear their browser data will lose progress.

Build docs developers (and LLMs) love