2024-12-29 16:28:57 -06:00
|
|
|
import React, { useState, useEffect, useCallback } from 'react';
|
2024-12-10 16:44:34 -06:00
|
|
|
import { Dialog } from 'primereact/dialog';
|
|
|
|
import Image from 'next/image';
|
2024-12-29 16:28:57 -06:00
|
|
|
import { useNDKContext } from '@/context/NDKContext';
|
|
|
|
import { useSession } from 'next-auth/react';
|
|
|
|
import { ProgressSpinner } from 'primereact/progressspinner';
|
|
|
|
import { nip19 } from 'nostr-tools';
|
2024-12-10 16:44:34 -06:00
|
|
|
|
|
|
|
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();
|
2024-12-29 16:28:57 -06:00
|
|
|
|
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;
|
2024-12-29 16:28:57 -06:00
|
|
|
|
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',
|
|
|
|
],
|
|
|
|
});
|
2024-12-29 16:28:57 -06:00
|
|
|
|
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],
|
|
|
|
});
|
2024-12-29 16:28:57 -06:00
|
|
|
|
2025-04-02 17:47:30 -05:00
|
|
|
// Create a map to store the latest badge for each definition
|
|
|
|
const latestBadgeMap = new Map();
|
2024-12-29 16:28:57 -06:00
|
|
|
|
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);
|
|
|
|
});
|
2024-12-29 16:28:57 -06:00
|
|
|
|
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],
|
|
|
|
}),
|
|
|
|
};
|
2024-12-29 16:28:57 -06:00
|
|
|
|
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);
|
|
|
|
}
|
2024-12-10 16:44:34 -06:00
|
|
|
}
|
2025-04-02 17:47:30 -05:00
|
|
|
}
|
2024-12-10 16:44:34 -06: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>
|
2024-12-10 16:44:34 -06:00
|
|
|
|
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>
|
|
|
|
);
|
2024-12-10 16:44:34 -06:00
|
|
|
};
|
|
|
|
|
|
|
|
export default UserBadges;
|