194 lines
7.3 KiB
TypeScript
194 lines
7.3 KiB
TypeScript
import Image from "next/image";
|
|
import { useState, useEffect } from "react";
|
|
import { API_BASE_URL } from "@/data/shared";
|
|
import { PlusCircleIcon, XCircleIcon, UserPlusIcon, TrophyIcon } from '@heroicons/react/24/outline';
|
|
|
|
interface Activity {
|
|
id: string;
|
|
type: string;
|
|
owner_id: string;
|
|
joiner_id: string | null;
|
|
game: string;
|
|
amount: string;
|
|
comments: string | null;
|
|
time: string;
|
|
}
|
|
|
|
interface UserData {
|
|
id: string;
|
|
username: string;
|
|
bio: string;
|
|
x_profile_url: string;
|
|
}
|
|
|
|
export default function Activities() {
|
|
const [activities, setActivities] = useState<Activity[]>([]);
|
|
const [userData, setUserData] = useState<Record<string, UserData>>({});
|
|
const [loading, setLoading] = useState(true);
|
|
const [failedImages, setFailedImages] = useState<Set<string>>(new Set());
|
|
const defaultPFP = '/duelfiassets/PFP (1).png';
|
|
|
|
useEffect(() => {
|
|
const fetchActivities = async () => {
|
|
try {
|
|
const response = await fetch("/api/v1/get_activities.php");
|
|
const data = await response.json();
|
|
setActivities(data);
|
|
|
|
// Fetch user data for all unique user IDs
|
|
const userIds = new Set<string>();
|
|
data.forEach((activity: Activity) => {
|
|
if (activity.owner_id) userIds.add(activity.owner_id);
|
|
if (activity.joiner_id) userIds.add(activity.joiner_id);
|
|
});
|
|
|
|
const userDataPromises = Array.from(userIds).map(async (userId) => {
|
|
try {
|
|
console.log('Fetching user data for ID:', userId);
|
|
|
|
const response = await fetch(`/api/v1/get_user_by_id.php?id=${userId}`);
|
|
const data = await response.json();
|
|
console.log('Received user data:', data);
|
|
return [userId, data];
|
|
} catch (error) {
|
|
console.error(`Error fetching user data for ${userId}:`, error);
|
|
return [userId, null];
|
|
}
|
|
});
|
|
|
|
const userDataResults = await Promise.all(userDataPromises);
|
|
console.log('All user data results:', userDataResults);
|
|
const userDataMap = Object.fromEntries(userDataResults);
|
|
console.log('Final user data map:', userDataMap);
|
|
setUserData(userDataMap);
|
|
} catch (error) {
|
|
console.error("Error fetching activities:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// Initial fetch
|
|
fetchActivities();
|
|
|
|
// Set up interval for periodic updates
|
|
const intervalId = setInterval(fetchActivities, 5000);
|
|
|
|
// Cleanup function to clear the interval when component unmounts
|
|
return () => clearInterval(intervalId);
|
|
}, []);
|
|
|
|
const formatActivityMessage = (activity: Activity) => {
|
|
const ownerUsername = userData[activity.owner_id]?.username || "Anonymous";
|
|
const joinerUsername = activity.joiner_id ? (userData[activity.joiner_id]?.username || "Anonymous") : null;
|
|
const amount = parseFloat(activity.amount);
|
|
const formattedAmount = amount > 0 ? `${amount} SOL` : "";
|
|
|
|
switch (activity.type) {
|
|
case "create":
|
|
return <><span className="font-bold">{ownerUsername}</span> created a <span className="font-bold">{activity.game}</span> game with a {formattedAmount} entry fee.</>;
|
|
case "close":
|
|
return <><span className="font-bold">{ownerUsername}</span> cancelled their <span className="font-bold">{activity.game}</span> game.</>;
|
|
case "join":
|
|
return <><span className="font-bold">{joinerUsername}</span> joined <span className="font-bold">{activity.game}</span> with a {formattedAmount} entry fee.</>;
|
|
case "won":
|
|
return <><span className="font-bold">{ownerUsername}</span> won {formattedAmount} in <span className="font-bold">{activity.game}</span>!</>;
|
|
default:
|
|
return "Unknown activity";
|
|
}
|
|
};
|
|
|
|
const formatTimeAgo = (timestamp: string) => {
|
|
const date = new Date(timestamp + 'Z');
|
|
const now = new Date();
|
|
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
|
|
const diffInMinutes = Math.floor(diffInSeconds / 60);
|
|
const diffInHours = Math.floor(diffInMinutes / 60);
|
|
const diffInDays = Math.floor(diffInHours / 24);
|
|
|
|
if (diffInDays > 0) {
|
|
return date.toLocaleString(undefined, {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
hour12: true
|
|
});
|
|
}
|
|
|
|
if (diffInHours > 0) {
|
|
return `${diffInHours} hour${diffInHours === 1 ? '' : 's'} ago`;
|
|
}
|
|
|
|
if (diffInMinutes > 0) {
|
|
return `${diffInMinutes} minute${diffInMinutes === 1 ? '' : 's'} ago`;
|
|
}
|
|
|
|
return "less than 1 minute ago";
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="container mx-auto max-w-screen-xl px-3">
|
|
<div className="bg-[rgb(30,30,30)] rounded-xl p-6 shadow-lg">
|
|
<h2 className="text-2xl font-bold text-white mb-4">Recent Activities</h2>
|
|
<div className="flex justify-center items-center h-40">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-[rgb(248,144,22)]"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="container mx-auto max-w-screen-xl px-3 md:px-40">
|
|
<div className="bg-[rgb(30,30,30)] rounded-xl p-6 shadow-lg">
|
|
<h2 className="text-2xl font-bold text-white mb-4">Recent Activities</h2>
|
|
<div className="space-y-4">
|
|
{[...activities].sort((a, b) => new Date(b.time).getTime() - new Date(a.time).getTime()).map((activity) => {
|
|
const userId = activity.type === "join" ? activity.joiner_id : activity.owner_id;
|
|
if (!userId) return null;
|
|
|
|
let profileUrl = userData[userId]?.x_profile_url || `${API_BASE_URL}profile_pics/${userId}.jpg`;
|
|
if (failedImages.has(profileUrl)) {
|
|
profileUrl = defaultPFP;
|
|
}
|
|
|
|
return (
|
|
<div
|
|
key={activity.id}
|
|
className="flex items-center gap-4 p-3 px-6 rounded-lg bg-[rgb(10,10,10)] border border-[rgb(30,30,30)] hover:border-[rgb(248,144,22)] transition-all duration-300"
|
|
>
|
|
<Image
|
|
src={profileUrl}
|
|
alt="Profile"
|
|
width={40}
|
|
height={40}
|
|
className="rounded-full border border-gray-700 object-cover"
|
|
onError={(e) => {
|
|
// @ts-expect-error - Type mismatch expected
|
|
e.target.src = defaultPFP;
|
|
setFailedImages(prev => new Set(prev).add(profileUrl));
|
|
}}
|
|
/>
|
|
<div className="flex-1">
|
|
<p className="text-white">{formatActivityMessage(activity)}</p>
|
|
<p className="text-sm text-gray-400">
|
|
{formatTimeAgo(activity.time)}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
{activity.type === "create" && <PlusCircleIcon className="w-6 h-6 text-green-500" />}
|
|
{activity.type === "close" && <XCircleIcon className="w-6 h-6 text-red-500" />}
|
|
{activity.type === "join" && <UserPlusIcon className="w-6 h-6 text-[rgb(248,144,22)]" />}
|
|
{activity.type === "won" && <TrophyIcon className="w-6 h-6 text-[rgb(248,144,22)]" />}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|