Skip to main content

Overview

Create a complete achievement system that tracks user progress, displays achievements, and handles claiming.

Basic Achievement System

Achievement Display

import { ZKScoreClient } from '@zkscore/sdk';

const client = new ZKScoreClient({
  apiKey: process.env.ZKSCORE_API_KEY
});

class AchievementSystem {
  constructor() {
    this.client = client;
  }
  
  async getUserAchievements(userAddress) {
    try {
      const achievements = await this.client.getAchievements(userAddress);
      return achievements.map(achievement => ({
        id: achievement.id,
        name: achievement.name,
        description: achievement.description,
        category: achievement.category,
        rarity: achievement.rarity,
        points: achievement.points,
        claimedAt: achievement.claimedAt,
        imageUrl: achievement.imageUrl
      }));
    } catch (error) {
      console.error('Error getting achievements:', error);
      return [];
    }
  }
  
  async getAvailableAchievements(userAddress) {
    try {
      const allAchievements = await this.client.listAchievements();
      const userAchievements = await this.getUserAchievements(userAddress);
      const userAchievementIds = userAchievements.map(a => a.id);
      
      return allAchievements.filter(achievement => 
        !userAchievementIds.includes(achievement.id)
      );
    } catch (error) {
      console.error('Error getting available achievements:', error);
      return [];
    }
  }
}

Achievement Progress Tracking

class AchievementProgress {
  constructor(client) {
    this.client = client;
  }
  
  async getAchievementProgress(userAddress, achievementId) {
    try {
      const progress = await this.client.getAchievementProgress(userAddress, achievementId);
      return {
        achievementId,
        current: progress.current,
        required: progress.required,
        percentage: (progress.current / progress.required) * 100,
        isComplete: progress.current >= progress.required
      };
    } catch (error) {
      console.error('Error getting progress:', error);
      return null;
    }
  }
  
  async getAllProgress(userAddress) {
    try {
      const availableAchievements = await this.getAvailableAchievements(userAddress);
      const progressPromises = availableAchievements.map(achievement => 
        this.getAchievementProgress(userAddress, achievement.id)
      );
      
      return await Promise.all(progressPromises);
    } catch (error) {
      console.error('Error getting all progress:', error);
      return [];
    }
  }
}

React Achievement Component

Achievement List Component

import React, { useState, useEffect } from 'react';
import { ZKScoreClient } from '@zkscore/sdk';

const AchievementList = ({ userAddress }) => {
  const [achievements, setAchievements] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    loadAchievements();
  }, [userAddress]);
  
  const loadAchievements = async () => {
    try {
      setLoading(true);
      const client = new ZKScoreClient({
        apiKey: process.env.REACT_APP_ZKSCORE_API_KEY
      });
      
      const userAchievements = await client.getAchievements(userAddress);
      setAchievements(userAchievements);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };
  
  if (loading) return <div>Loading achievements...</div>;
  if (error) return <div>Error: {error}</div>;
  
  return (
    <div className="achievement-list">
      <h2>Achievements ({achievements.length})</h2>
      {achievements.map(achievement => (
        <AchievementCard 
          key={achievement.id} 
          achievement={achievement} 
        />
      ))}
    </div>
  );
};

const AchievementCard = ({ achievement }) => {
  const getRarityColor = (rarity) => {
    const colors = {
      common: '#gray',
      uncommon: '#green',
      rare: '#blue',
      epic: '#purple',
      legendary: '#gold'
    };
    return colors[rarity] || '#gray';
  };
  
  return (
    <div 
      className="achievement-card"
      style={{ borderColor: getRarityColor(achievement.rarity) }}
    >
      <div className="achievement-header">
        <h3>{achievement.name}</h3>
        <span className={`rarity ${achievement.rarity}`}>
          {achievement.rarity.toUpperCase()}
        </span>
      </div>
      <p>{achievement.description}</p>
      <div className="achievement-meta">
        <span>Points: {achievement.points}</span>
        <span>Category: {achievement.category}</span>
        <span>Claimed: {new Date(achievement.claimedAt).toLocaleDateString()}</span>
      </div>
    </div>
  );
};

Achievement Progress Component

const AchievementProgress = ({ userAddress }) => {
  const [progress, setProgress] = useState([]);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    loadProgress();
  }, [userAddress]);
  
  const loadProgress = async () => {
    try {
      setLoading(true);
      const client = new ZKScoreClient({
        apiKey: process.env.REACT_APP_ZKSCORE_API_KEY
      });
      
      const availableAchievements = await client.listAchievements();
      const progressPromises = availableAchievements.map(async (achievement) => {
        const progress = await client.getAchievementProgress(userAddress, achievement.id);
        return { ...achievement, progress };
      });
      
      const allProgress = await Promise.all(progressPromises);
      setProgress(allProgress.filter(p => !p.progress.isComplete));
    } catch (error) {
      console.error('Error loading progress:', error);
    } finally {
      setLoading(false);
    }
  };
  
  if (loading) return <div>Loading progress...</div>;
  
  return (
    <div className="achievement-progress">
      <h2>Progress ({progress.length})</h2>
      {progress.map(item => (
        <ProgressCard 
          key={item.id} 
          achievement={item} 
          progress={item.progress}
        />
      ))}
    </div>
  );
};

const ProgressCard = ({ achievement, progress }) => {
  const percentage = (progress.current / progress.required) * 100;
  
  return (
    <div className="progress-card">
      <div className="progress-header">
        <h3>{achievement.name}</h3>
        <span>{progress.current}/{progress.required}</span>
      </div>
      <div className="progress-bar">
        <div 
          className="progress-fill"
          style={{ width: `${percentage}%` }}
        />
      </div>
      <p>{achievement.description}</p>
    </div>
  );
};

Achievement Claiming

Claim Achievement Function

class AchievementClaimer {
  constructor(client) {
    this.client = client;
  }
  
  async claimAchievement(userAddress, achievementId) {
    try {
      // Check if user can claim
      const canClaim = await this.client.canClaimAchievement(userAddress, achievementId);
      if (!canClaim) {
        throw new Error('Cannot claim this achievement');
      }
      
      // Claim achievement
      const result = await this.client.claimAchievement(userAddress, achievementId);
      
      return {
        success: true,
        achievementId,
        transactionHash: result.transactionHash
      };
    } catch (error) {
      return {
        success: false,
        error: error.message
      };
    }
  }
  
  async claimAllEligibleAchievements(userAddress) {
    try {
      const availableAchievements = await this.client.listAchievements();
      const claimPromises = availableAchievements.map(async (achievement) => {
        const canClaim = await this.client.canClaimAchievement(userAddress, achievement.id);
        if (canClaim) {
          return this.claimAchievement(userAddress, achievement.id);
        }
        return null;
      });
      
      const results = await Promise.all(claimPromises);
      return results.filter(result => result !== null);
    } catch (error) {
      console.error('Error claiming achievements:', error);
      return [];
    }
  }
}

Achievement Notifications

Real-time Notifications

class AchievementNotifier {
  constructor(client) {
    this.client = client;
    this.listeners = [];
  }
  
  subscribe(callback) {
    this.listeners.push(callback);
  }
  
  unsubscribe(callback) {
    this.listeners = this.listeners.filter(listener => listener !== callback);
  }
  
  notify(achievement) {
    this.listeners.forEach(callback => callback(achievement));
  }
  
  async checkForNewAchievements(userAddress) {
    try {
      const achievements = await this.client.getAchievements(userAddress);
      const latestAchievement = achievements[achievements.length - 1];
      
      if (latestAchievement) {
        const now = Date.now();
        const claimedAt = new Date(latestAchievement.claimedAt).getTime();
        
        // If claimed within last 5 minutes, notify
        if (now - claimedAt < 5 * 60 * 1000) {
          this.notify(latestAchievement);
        }
      }
    } catch (error) {
      console.error('Error checking achievements:', error);
    }
  }
}

// Usage
const notifier = new AchievementNotifier(client);
notifier.subscribe((achievement) => {
  showNotification(`Achievement unlocked: ${achievement.name}!`);
});

Best Practices

  1. Progress Tracking: Always show progress toward achievements
  2. Real-time Updates: Use event listeners for live updates
  3. Error Handling: Handle claim failures gracefully
  4. User Feedback: Provide clear feedback on achievement status
  5. Performance: Cache achievement data to reduce API calls