Update activity tab to fetch real leaderboard data from API
Replaces mock leaderboard data with dynamic API fetching and adds loading and empty states to the LeaderboardTab component in client/pages/Activity.tsx. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9203795e-937a-4306-b81d-b4d5c78c240e Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 5fa6da1f-e1dc-40f2-b0b8-5dc4a160c3b7 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7c94b7a0-29c7-4f2e-94ef-44b2153872b7/9203795e-937a-4306-b81d-b4d5c78c240e/139vJay Replit-Helium-Checkpoint-Created: true
This commit is contained in:
parent
210fd1f556
commit
02134b613b
1 changed files with 258 additions and 86 deletions
|
|
@ -346,18 +346,50 @@ function RealmsTab({ currentRealm, openExternalLink }: { currentRealm: ArmType;
|
|||
}
|
||||
|
||||
function LeaderboardTab({ openExternalLink, currentUserId }: { openExternalLink: (url: string) => Promise<void>; currentUserId?: string }) {
|
||||
const leaderboard: LeaderboardEntry[] = [
|
||||
{ rank: 1, user_id: "1", username: "PixelMaster", total_xp: 15420, level: 16, current_streak: 21 },
|
||||
{ rank: 2, user_id: "2", username: "CodeNinja", total_xp: 12800, level: 13, current_streak: 14 },
|
||||
{ rank: 3, user_id: "3", username: "BuilderX", total_xp: 11250, level: 12, current_streak: 7 },
|
||||
{ rank: 4, user_id: "4", username: "GameDev_Pro", total_xp: 9800, level: 10, current_streak: 5 },
|
||||
{ rank: 5, user_id: "5", username: "ForgeHero", total_xp: 8500, level: 9, current_streak: 12 },
|
||||
{ rank: 6, user_id: "6", username: "NexusCreator", total_xp: 7200, level: 8, current_streak: 3 },
|
||||
{ rank: 7, user_id: "7", username: "LabsExplorer", total_xp: 6100, level: 7 },
|
||||
];
|
||||
const [leaderboard, setLeaderboard] = useState<LeaderboardEntry[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [userRank, setUserRank] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchLeaderboard = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/activity/leaderboard');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const entries = (data.data || data || []).map((e: any, i: number) => ({
|
||||
rank: i + 1,
|
||||
user_id: e.user_id || e.id,
|
||||
username: e.username || e.full_name || 'Anonymous',
|
||||
avatar_url: e.avatar_url,
|
||||
total_xp: e.total_xp || 0,
|
||||
level: e.level || Math.floor((e.total_xp || 0) / 1000) + 1,
|
||||
current_streak: e.current_streak || 0,
|
||||
}));
|
||||
setLeaderboard(entries);
|
||||
if (currentUserId) {
|
||||
const userIdx = entries.findIndex((e: any) => e.user_id === currentUserId);
|
||||
if (userIdx >= 0) setUserRank(userIdx + 1);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
setLeaderboard([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchLeaderboard();
|
||||
}, [currentUserId]);
|
||||
|
||||
const medals = ["🥇", "🥈", "🥉"];
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center py-8">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-purple-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
|
|
@ -371,13 +403,19 @@ function LeaderboardTab({ openExternalLink, currentUserId }: { openExternalLink:
|
|||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-white text-sm font-medium">Your Rank</span>
|
||||
<span className="text-purple-300 font-bold">#12</span>
|
||||
<span className="text-purple-300 font-bold">{userRank ? `#${userRank}` : 'Unranked'}</span>
|
||||
</div>
|
||||
<p className="text-[#949ba4] text-xs mt-1">Keep earning XP to climb!</p>
|
||||
</motion.div>
|
||||
|
||||
{leaderboard.length === 0 && (
|
||||
<div className="text-center py-6">
|
||||
<Trophy className="w-8 h-8 text-[#4e5058] mx-auto mb-2" />
|
||||
<p className="text-[#949ba4] text-sm">No leaderboard data yet</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(
|
||||
leaderboard.map((entry, index) => (
|
||||
{leaderboard.map((entry, index) => (
|
||||
<motion.div
|
||||
key={entry.user_id}
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
|
|
@ -1153,42 +1191,80 @@ interface AethexEvent {
|
|||
}
|
||||
|
||||
function EventCalendarTab({ userId, openExternalLink }: { userId?: string; openExternalLink: (url: string) => Promise<void> }) {
|
||||
const [rsvpEvents, setRsvpEvents] = useState<Set<string>>(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem('aethex_event_rsvps');
|
||||
return new Set(saved ? JSON.parse(saved) : []);
|
||||
} catch {
|
||||
return new Set();
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('aethex_event_rsvps', JSON.stringify(Array.from(rsvpEvents)));
|
||||
}, [rsvpEvents]);
|
||||
const [events, setEvents] = useState<AethexEvent[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [rsvpEvents, setRsvpEvents] = useState<Set<string>>(new Set());
|
||||
const [rsvping, setRsvping] = useState<string | null>(null);
|
||||
|
||||
const now = Date.now();
|
||||
const hour = 60 * 60 * 1000;
|
||||
const day = 24 * hour;
|
||||
|
||||
const events: AethexEvent[] = [
|
||||
{ id: 'e1', title: 'GameForge Weekly Showcase', description: 'Present your latest game projects', type: 'stream', startTime: now + 2 * hour, endTime: now + 4 * hour, host: 'AeThex Team', realm: 'gameforge', attendees: 45, maxAttendees: 100 },
|
||||
{ id: 'e2', title: 'Unity Workshop: VFX Basics', description: 'Learn to create stunning visual effects', type: 'workshop', startTime: now + 1 * day, endTime: now + 1 * day + 2 * hour, host: 'PixelMaster', realm: 'gameforge', attendees: 28, maxAttendees: 50 },
|
||||
{ id: 'e3', title: 'Labs Demo Day', description: 'R&D team demos experimental features', type: 'stream', startTime: now + 2 * day, endTime: now + 2 * day + 3 * hour, host: 'Labs Team', realm: 'labs', attendees: 62 },
|
||||
{ id: 'e4', title: 'Community AMA', description: 'Ask us anything about AeThex platform', type: 'ama', startTime: now + 3 * day, endTime: now + 3 * day + hour, host: 'Founders', realm: 'foundation', attendees: 89 },
|
||||
{ id: 'e5', title: 'Pixel Art Tournament', description: 'Create the best pixel art in 2 hours', type: 'tournament', startTime: now + 4 * day, endTime: now + 4 * day + 2 * hour, host: 'ArtistX', realm: 'nexus', attendees: 34, maxAttendees: 64 },
|
||||
];
|
||||
|
||||
const toggleRsvp = (eventId: string) => {
|
||||
if (!userId) return;
|
||||
setRsvpEvents(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(eventId)) {
|
||||
next.delete(eventId);
|
||||
} else {
|
||||
next.add(eventId);
|
||||
useEffect(() => {
|
||||
const fetchEvents = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/activity/events');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const mapped = (data.data || data || []).map((e: any) => ({
|
||||
id: e.id,
|
||||
title: e.title,
|
||||
description: e.description || '',
|
||||
type: e.event_type || 'stream',
|
||||
startTime: new Date(e.start_time).getTime(),
|
||||
endTime: new Date(e.end_time).getTime(),
|
||||
host: e.host_name || 'AeThex',
|
||||
realm: e.realm || 'nexus',
|
||||
attendees: e.attendee_count || 0,
|
||||
maxAttendees: e.max_attendees,
|
||||
}));
|
||||
setEvents(mapped);
|
||||
const userRsvps = (data.data || data || [])
|
||||
.filter((e: any) => e.user_rsvp)
|
||||
.map((e: any) => e.id);
|
||||
setRsvpEvents(new Set(userRsvps));
|
||||
}
|
||||
} catch {
|
||||
setEvents([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
fetchEvents();
|
||||
}, []);
|
||||
|
||||
const toggleRsvp = async (eventId: string) => {
|
||||
if (!userId || rsvping) return;
|
||||
setRsvping(eventId);
|
||||
const hasRsvp = rsvpEvents.has(eventId);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/activity/events/${eventId}/rsvp`, {
|
||||
method: hasRsvp ? 'DELETE' : 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ user_id: userId }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setRsvpEvents(prev => {
|
||||
const next = new Set(prev);
|
||||
if (hasRsvp) {
|
||||
next.delete(eventId);
|
||||
} else {
|
||||
next.add(eventId);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
setEvents(prev => prev.map(e =>
|
||||
e.id === eventId
|
||||
? { ...e, attendees: e.attendees + (hasRsvp ? -1 : 1) }
|
||||
: e
|
||||
));
|
||||
}
|
||||
} catch {
|
||||
} finally {
|
||||
setRsvping(null);
|
||||
}
|
||||
};
|
||||
|
||||
const formatEventTime = (startTime: number) => {
|
||||
|
|
@ -1209,6 +1285,14 @@ function EventCalendarTab({ userId, openExternalLink }: { userId?: string; openE
|
|||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center py-8">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-blue-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
|
|
@ -1227,6 +1311,14 @@ function EventCalendarTab({ userId, openExternalLink }: { userId?: string; openE
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{events.length === 0 && (
|
||||
<div className="text-center py-6">
|
||||
<Calendar className="w-8 h-8 text-[#4e5058] mx-auto mb-2" />
|
||||
<p className="text-[#949ba4] text-sm">No upcoming events</p>
|
||||
<p className="text-[#4e5058] text-xs mt-1">Check back later for new events!</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{events.map((event, index) => {
|
||||
const config = ARM_CONFIG[event.realm];
|
||||
const EventIcon = getEventIcon(event.type);
|
||||
|
|
@ -1316,30 +1408,62 @@ interface TeamListing {
|
|||
}
|
||||
|
||||
function TeamFinderTab({ userId, openExternalLink }: { userId?: string; openExternalLink: (url: string) => Promise<void> }) {
|
||||
const [appliedTeams, setAppliedTeams] = useState<Set<string>>(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem('aethex_team_applications');
|
||||
return new Set(saved ? JSON.parse(saved) : []);
|
||||
} catch {
|
||||
return new Set();
|
||||
}
|
||||
});
|
||||
const [showCreateForm, setShowCreateForm] = useState(false);
|
||||
const [listings, setListings] = useState<TeamListing[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [appliedTeams, setAppliedTeams] = useState<Set<string>>(new Set());
|
||||
const [applying, setApplying] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('aethex_team_applications', JSON.stringify(Array.from(appliedTeams)));
|
||||
}, [appliedTeams]);
|
||||
const fetchTeams = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/activity/teams');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const mapped = (data.data || data || []).map((t: any) => ({
|
||||
id: t.id,
|
||||
projectTitle: t.project_title || t.title,
|
||||
description: t.description || '',
|
||||
creator: t.creator_name || 'Unknown',
|
||||
creatorAvatar: t.creator_avatar,
|
||||
realm: t.realm || 'gameforge',
|
||||
rolesNeeded: t.roles_needed || [],
|
||||
teamSize: t.team_size || 1,
|
||||
maxTeam: t.max_team || 5,
|
||||
createdAt: new Date(t.created_at).getTime(),
|
||||
}));
|
||||
setListings(mapped);
|
||||
const userApps = (data.data || data || [])
|
||||
.filter((t: any) => t.user_applied)
|
||||
.map((t: any) => t.id);
|
||||
setAppliedTeams(new Set(userApps));
|
||||
}
|
||||
} catch {
|
||||
setListings([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchTeams();
|
||||
}, []);
|
||||
|
||||
const listings: TeamListing[] = [
|
||||
{ id: 't1', projectTitle: 'Neon Runners', description: 'Looking for 2D animator and sound designer for cyberpunk runner game', creator: 'GameDev_Pro', realm: 'gameforge', rolesNeeded: ['2D Animator', 'Sound Designer'], teamSize: 3, maxTeam: 5, createdAt: Date.now() - 2 * 60 * 60 * 1000 },
|
||||
{ id: 't2', projectTitle: 'EcoSim', description: 'Need backend dev for environmental simulation game', creator: 'GreenCoder', realm: 'gameforge', rolesNeeded: ['Backend Dev'], teamSize: 2, maxTeam: 4, createdAt: Date.now() - 5 * 60 * 60 * 1000 },
|
||||
{ id: 't3', projectTitle: 'AI Art Tool', description: 'Building an AI-powered pixel art generator, need ML engineer', creator: 'PixelMaster', realm: 'labs', rolesNeeded: ['ML Engineer', 'UI Designer'], teamSize: 1, maxTeam: 3, createdAt: Date.now() - 12 * 60 * 60 * 1000 },
|
||||
{ id: 't4', projectTitle: 'SoundBoard Pro', description: 'Audio production tool needs React developer', creator: 'BeatMaker', realm: 'nexus', rolesNeeded: ['React Dev', 'Audio Engineer'], teamSize: 2, maxTeam: 4, createdAt: Date.now() - 1 * 24 * 60 * 60 * 1000 },
|
||||
];
|
||||
|
||||
const applyToTeam = (teamId: string) => {
|
||||
if (!userId || appliedTeams.has(teamId)) return;
|
||||
setAppliedTeams(prev => new Set(prev).add(teamId));
|
||||
const applyToTeam = async (teamId: string) => {
|
||||
if (!userId || appliedTeams.has(teamId) || applying) return;
|
||||
setApplying(teamId);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/activity/teams/${teamId}/apply`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ user_id: userId }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setAppliedTeams(prev => new Set(prev).add(teamId));
|
||||
}
|
||||
} catch {
|
||||
} finally {
|
||||
setApplying(null);
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (timestamp: number) => {
|
||||
|
|
@ -1350,6 +1474,14 @@ function TeamFinderTab({ userId, openExternalLink }: { userId?: string; openExte
|
|||
return `${Math.floor(hours / 24)}d ago`;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center py-8">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-green-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
|
|
@ -1376,6 +1508,14 @@ function TeamFinderTab({ userId, openExternalLink }: { userId?: string; openExte
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{listings.length === 0 && (
|
||||
<div className="text-center py-6">
|
||||
<UserPlus className="w-8 h-8 text-[#4e5058] mx-auto mb-2" />
|
||||
<p className="text-[#949ba4] text-sm">No teams looking for members</p>
|
||||
<p className="text-[#4e5058] text-xs mt-1">Be the first to post a team listing!</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{listings.map((listing, index) => {
|
||||
const config = ARM_CONFIG[listing.realm];
|
||||
const hasApplied = appliedTeams.has(listing.id);
|
||||
|
|
@ -2062,34 +2202,66 @@ interface Project {
|
|||
}
|
||||
|
||||
function ProjectsTab({ userId, openExternalLink }: { userId?: string; openExternalLink: (url: string) => Promise<void> }) {
|
||||
const [votedProjects, setVotedProjects] = useState<Set<string>>(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem('aethex_voted_projects');
|
||||
return new Set(saved ? JSON.parse(saved) : []);
|
||||
} catch {
|
||||
return new Set();
|
||||
}
|
||||
});
|
||||
const [projects, setProjects] = useState<Project[]>([
|
||||
{ id: 'p1', title: 'Neon Racer', description: 'A cyberpunk racing game with stunning visuals', author: 'PixelDev', realm: 'gameforge', thumbnail: '🏎️', votes: 142, views: 890, tags: ['Game', 'Racing'] },
|
||||
{ id: 'p2', title: 'DevFlow', description: 'AI-powered code review and workflow tool', author: 'CodeMaster', realm: 'labs', thumbnail: '🔮', votes: 98, views: 654, tags: ['Tool', 'AI'] },
|
||||
{ id: 'p3', title: 'EcoTrack', description: 'Track and reduce your carbon footprint', author: 'GreenCoder', realm: 'foundation', thumbnail: '🌱', votes: 76, views: 432, tags: ['Social', 'Environment'] },
|
||||
{ id: 'p4', title: 'SoundWave', description: 'Collaborative music production platform', author: 'BeatMaker', realm: 'nexus', thumbnail: '🎵', votes: 112, views: 780, tags: ['Music', 'Collaboration'] },
|
||||
{ id: 'p5', title: 'PixelForge', description: '2D game asset generator using AI', author: 'ArtistX', realm: 'gameforge', thumbnail: '🎨', votes: 89, views: 567, tags: ['Tool', 'Art'] },
|
||||
]);
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [votedProjects, setVotedProjects] = useState<Set<string>>(new Set());
|
||||
const [voting, setVoting] = useState<string | null>(null);
|
||||
const [filter, setFilter] = useState<ArmType | 'all'>('all');
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('aethex_voted_projects', JSON.stringify(Array.from(votedProjects)));
|
||||
}, [votedProjects]);
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/activity/projects');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const mapped = (data.data || data || []).map((p: any) => ({
|
||||
id: p.id,
|
||||
title: p.title || p.name,
|
||||
description: p.description || '',
|
||||
author: p.author_name || p.creator_name || 'Anonymous',
|
||||
authorAvatar: p.author_avatar,
|
||||
realm: p.realm || 'nexus',
|
||||
thumbnail: p.thumbnail || p.image_url || '🚀',
|
||||
votes: p.upvote_count || p.votes || 0,
|
||||
views: p.view_count || p.views || 0,
|
||||
tags: p.tags || [],
|
||||
}));
|
||||
setProjects(mapped);
|
||||
const userVotes = (data.data || data || [])
|
||||
.filter((p: any) => p.user_voted)
|
||||
.map((p: any) => p.id);
|
||||
setVotedProjects(new Set(userVotes));
|
||||
}
|
||||
} catch {
|
||||
setProjects([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchProjects();
|
||||
}, []);
|
||||
|
||||
const voteProject = (projectId: string) => {
|
||||
if (!userId || votedProjects.has(projectId)) return;
|
||||
const voteProject = async (projectId: string) => {
|
||||
if (!userId || votedProjects.has(projectId) || voting) return;
|
||||
setVoting(projectId);
|
||||
|
||||
setVotedProjects(prev => new Set(prev).add(projectId));
|
||||
setProjects(prev => prev.map(p =>
|
||||
p.id === projectId ? { ...p, votes: p.votes + 1 } : p
|
||||
));
|
||||
try {
|
||||
const response = await fetch(`/api/activity/projects/${projectId}/upvote`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ user_id: userId }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setVotedProjects(prev => new Set(prev).add(projectId));
|
||||
setProjects(prev => prev.map(p =>
|
||||
p.id === projectId ? { ...p, votes: p.votes + 1 } : p
|
||||
));
|
||||
}
|
||||
} catch {
|
||||
} finally {
|
||||
setVoting(null);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredProjects = filter === 'all'
|
||||
|
|
|
|||
Loading…
Reference in a new issue