plebdevs/src/components/profile/UserBadges.js

155 lines
5.7 KiB
JavaScript
Raw Normal View History

import React, { useState, useEffect, useCallback } from 'react';
import { Dialog } from 'primereact/dialog';
import Image from 'next/image';
import { useNDKContext } from '@/context/NDKContext';
import { useSession } from 'next-auth/react';
import { ProgressSpinner } from 'primereact/progressspinner';
import { nip19 } from 'nostr-tools';
const UserBadges = ({ visible, onHide }) => {
2025-04-02 17:47:30 -05:00
const [badges, setBadges] = useState([]);
const [loading, setLoading] = useState(true);
const { ndk } = useNDKContext();
const { data: session } = useSession();
2025-04-02 17:47:30 -05:00
// Define fetchBadges as a useCallback to prevent unnecessary recreations
const fetchBadges = useCallback(async () => {
if (!ndk || !session?.user?.pubkey) return;
2025-04-02 17:47:30 -05:00
setLoading(true);
try {
// Fetch badge definitions (kind 30009)
const badgeDefinitions = await ndk.fetchEvents({
// todo: add the plebdevs hardcoded badge ids (probably in config?)
ids: [
'4054a68f028edf38cd1d71cc4693d4ff5c9c54b0b44532361fe6abb29530cbf6',
'5d38fea9a3c1fb4c55c9635c3132d34608c91de640f772438faa1942677087a8',
'3ba20936d66523adb6d71793649bc77f3cea34f50c21ec7bb2c041f936022214',
'41edee5af6d4e833d11f9411c2c27cc48c14d2a3c7966ae7648568e825eda1ed',
],
});
2025-04-02 17:47:30 -05:00
// Fetch badge awards (kind 8) using fetchEvents instead of subscribe
const badgeAwards = await ndk.fetchEvents({
kinds: [8],
// todo: add the plebdevs author pubkey
authors: ['f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741'],
'#p': [session.user.pubkey],
});
2025-04-02 17:47:30 -05:00
// Create a map to store the latest badge for each definition
const latestBadgeMap = new Map();
2025-04-02 17:47:30 -05:00
// Process all awards
for (const award of badgeAwards) {
const definition = Array.from(badgeDefinitions).find(def => {
const defDTag = def.tags.find(t => t[0] === 'd')?.[1];
const awardATag = award.tags.find(t => t[0] === 'a')?.[1];
return awardATag?.includes(defDTag);
});
2025-04-02 17:47:30 -05:00
if (definition) {
const defId = definition.id;
const currentBadge = {
name: definition.tags.find(t => t[0] === 'name')?.[1] || 'Unknown Badge',
description: definition.tags.find(t => t[0] === 'description')?.[1] || '',
image: definition.tags.find(t => t[0] === 'image')?.[1] || '',
thumbnail: definition.tags.find(t => t[0] === 'thumb')?.[1] || '',
awardedOn: new Date(award.created_at * 1000).toISOString(),
nostrId: award.id,
naddr: nip19.naddrEncode({
pubkey: definition.pubkey,
kind: definition.kind,
identifier: definition.tags.find(t => t[0] === 'd')?.[1],
}),
};
2025-04-02 17:47:30 -05:00
// Only update if this is the first instance or if it's newer than the existing one
if (
!latestBadgeMap.has(defId) ||
new Date(currentBadge.awardedOn) > new Date(latestBadgeMap.get(defId).awardedOn)
) {
latestBadgeMap.set(defId, currentBadge);
}
}
2025-04-02 17:47:30 -05:00
}
2025-04-02 17:47:30 -05:00
// Convert map values to array for state update
setBadges(Array.from(latestBadgeMap.values()));
} catch (error) {
console.error('Error fetching badges:', error);
} finally {
setLoading(false);
}
}, [ndk, session?.user?.pubkey]);
// Initial fetch effect
useEffect(() => {
if (visible) {
fetchBadges();
}
}, [visible, fetchBadges]);
const formatDate = dateString => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
};
return (
<Dialog header="Your Badges" visible={visible} onHide={onHide} className="w-full max-w-3xl">
<div className="p-4">
{loading ? (
<div className="flex justify-center items-center h-40">
<ProgressSpinner />
</div>
) : badges.length === 0 ? (
<div className="text-center text-gray-400">
No badges earned yet. Get started on the Dev Journey to earn badges!
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{badges.map((badge, index) => (
<div
key={index}
className="bg-gray-800 rounded-xl p-6 flex flex-col items-center transform transition-all duration-200 hover:scale-[1.02] hover:shadow-lg"
>
<div className="relative w-32 h-32 mb-4">
<Image
src={badge.thumbnail || badge.image}
alt={badge.name}
layout="fill"
objectFit="contain"
/>
</div>
<h3 className="text-white font-semibold text-xl mb-2">{badge.name}</h3>
<p className="text-gray-400 text-center text-sm">{badge.description}</p>
<div className="mt-4 flex flex-col items-center gap-2 w-full">
<div className="bg-blue-500/10 text-blue-400 px-3 py-1 rounded-full text-sm">
Earned on {formatDate(badge.awardedOn)}
</div>
2025-04-02 17:47:30 -05:00
<a
href={`https://badges.page/a/${badge.naddr}`}
target="_blank"
rel="noopener noreferrer"
className="text-purple-400 hover:text-purple-300 text-sm flex items-center gap-1 transition-colors"
>
<i className="pi pi-external-link" />
View on Nostr
</a>
</div>
</div>
))}
</div>
)}
</div>
</Dialog>
);
};
export default UserBadges;