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
Metadata structure for achievement definitions:
Unique identifier for the achievement
Display name for the achievement
User-facing description shown when unlocked
Path to achievement icon image
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.
The achievement to unlock
Options object:
accumulative: If true, increments count on existing achievements
True if this was the first time unlocking (false if already unlocked)
Updated achievements state
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.
import { hasAchievement } from '@/components/gamification/achievementsStore';
if (hasAchievement('explorer')) {
// User has explored the site
}
trackVisit()
Tracks a page visit and updates visit statistics.
True if this is the first visit today (different from last visit date)
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.