Overview
Create a complete achievement system that tracks user progress, displays achievements, and handles claiming.Basic Achievement System
Achievement Display
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
- Progress Tracking: Always show progress toward achievements
- Real-time Updates: Use event listeners for live updates
- Error Handling: Handle claim failures gracefully
- User Feedback: Provide clear feedback on achievement status
- Performance: Cache achievement data to reduce API calls